From dc16b0e2c753e7736601651f815a5df60e77f144 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:55:37 +0300 Subject: [PATCH 01/11] chore(deps): bump phoenix_live_view from 1.2.8 to 1.2.9 (#14714) 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 e9fe371b391..fd8bb90d038 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.20", "455e48f7115eca98c9f2b0e7a152b5a2e8f2a8a4f964c96e95bd31645ee5fa59", [:mix], [], "hexpm", "940eafbfd8b14bee649f083bc11b3b54ec555b54c3e4ea8213351ff6fee39c10"}, + "castore": {:hex, :castore, "1.0.21", "0a0e8330dc267a40a3b7ad86d39302764bb71758172904e6a59d5ad6443ce307", [:mix], [], "hexpm", "e42e22723e25dbd46876d056a03f685513d6e98f6b5e555dc551321decd76c5c"}, "cbor": {:hex, :cbor, "1.0.2", "9b0af85af291a556e10a0ffd48ba9a21a75e711828fafd3af193d56d95f0907f", [:mix], [], "hexpm", "edbc9b4a16eb93a582437b9b249c340a75af03958e338fb43d8c1be9fc65b864"}, "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"}, @@ -136,7 +136,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.2.8", "5006fd7b429c42489600fbc1600c750d0f0e5b5ea4965d9758e429b979392991", [: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", [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", "b05ffe21f43c0ff219da62948b482c324aa5b8873e17b0c0cac58289a178af38"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.9", "d35ddac2fab4480e6bdbf712ff104fd9e969a2bbe81b578ee80f066b371f56db", [: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", [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", "2f9528c3d7046edabbb30a91710ca33988f8d8bc20a964a1fc48b32134572afa"}, "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 d8baca6e92060cd019d1a5ca763acf9ce769c70d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 19 Aug 2026 13:04:55 +0300 Subject: [PATCH 02/11] refactor: switch TAC operations in search to Read API v2 (#14719) --- .gitignore | 1 + .../controllers/api/v2/search_controller.ex | 2 +- .../schemas/api/v2/search/result_item.ex | 23 + .../schemas/api/v2/search/results.ex | 3 +- .../schemas/api/v2/search/tac_operation.ex | 93 ++++ .../api/v2/search_controller_test.exs | 515 +++++++++--------- .../tac_operation_lifecycle.ex | 18 +- cspell.json | 1 + 8 files changed, 394 insertions(+), 262 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/result_item.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/tac_operation.ex diff --git a/.gitignore b/.gitignore index d4ba21b88b1..0d1d5db737b 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ dump.rdb .cursorrules .elixir_ls .claude/settings.local.json +.claude/commands/review-pr.md **.dec** 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 f25a0f42e0f..26a0bf5e8f2 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 @@ -107,7 +107,7 @@ defmodule BlockScoutWeb.API.V2.SearchController do responses: [ ok: {"Quick search results.", "application/json", - %Schema{type: :array, items: %Schema{type: :object}, nullable: false}}, + %Schema{type: :array, items: Schemas.Search.ResultItem, nullable: false}}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/result_item.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/result_item.ex new file mode 100644 index 00000000000..aea2f36eca2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/result_item.ex @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LicenseRef-Blockscout +defmodule BlockScoutWeb.Schemas.API.V2.Search.ResultItem do + @moduledoc """ + This module defines the schema for a single item of the search results. + + Only `tac_operation` is typed so far. The other result types (address, block, transaction, + token, etc.) are not described individually yet, so additional properties are deliberately + allowed and this schema only constrains an item that carries a TAC operation. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Search.TacOperation + + OpenApiSpex.schema(%{ + title: "SearchResultItem", + description: "Single search result. The shape depends on `type`; only `tac_operation` results are fully described.", + type: :object, + properties: %{ + tac_operation: TacOperation + }, + required: [] + }) +end 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 index 8d8484375ac..7534eba4feb 100644 --- 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 @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Search.Results do """ require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.Search.ResultItem alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -12,7 +13,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Search.Results do description: "Search results containing blocks, transactions, and addresses", type: :object, properties: %{ - items: %Schema{type: :array, items: %Schema{type: :object}}, + items: %Schema{type: :array, items: ResultItem}, next_page_params: %Schema{type: :object, nullable: true, additionalProperties: true} }, required: [] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/tac_operation.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/tac_operation.ex new file mode 100644 index 00000000000..286ef8673e8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/tac_operation.ex @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: LicenseRef-Blockscout +defmodule BlockScoutWeb.Schemas.API.V2.Search.TacOperation do + @moduledoc """ + This module defines the schema for a TAC operation in the search results. + + The object is returned by the `tac-operation-lifecycle` microservice (Read API v2) and is + proxied verbatim, so this schema mirrors the microservice contract rather than describing a + Blockscout-owned struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + @type_enum [ + "UNKNOWN", + "TON_TAC_TON", + "TAC_TON", + "TON_TAC" + ] + + @status_enum [ + "pending", + "success", + "failed" + ] + + @blockchain_enum [ + "TAC", + "TON", + "UNKNOWN_BLOCKCHAIN" + ] + + @sender_schema %Schema{ + type: :object, + properties: %{ + address: %Schema{ + type: :string, + nullable: false, + description: "TAC (EVM) or TON address, depending on `blockchain`", + example: "EQDoF2OkxsI3gc5jAuxlqozN9H/SgEOUCopMa1yU4djLaXuL" + }, + blockchain: %Schema{type: :string, enum: @blockchain_enum, nullable: false, example: "TON"} + }, + required: [:address, :blockchain], + additionalProperties: false, + nullable: true + } + + OpenApiSpex.schema(%{ + title: "TacOperationSearchResult", + description: "TAC operation as returned by the tac-operation-lifecycle service Read API v2.", + type: :object, + properties: %{ + operation_id: %Schema{ + type: :string, + nullable: false, + example: "0xf01646ac36cbbcebd8a5ff09c300d5f3bebdd2fbe0135a377eaa485d9edcc670" + }, + type: %Schema{ + type: :string, + enum: @type_enum, + nullable: false, + description: "Transfer route. Never carries a lifecycle outcome — see `status` and `rollback`.", + example: "TAC_TON" + }, + status: %Schema{ + type: :string, + enum: @status_enum, + nullable: false, + description: "Business outcome of the operation.", + example: "success" + }, + rollback: %Schema{ + type: :boolean, + nullable: false, + description: "Whether a rollback occurred.", + example: false + }, + timestamp: General.Timestamp, + sender: @sender_schema, + error_reason: %Schema{ + type: :string, + nullable: true, + description: + "Short failure label. It is published only when the stored reason is short enough to be a label, so it is legitimately `null` on a failed operation — its absence does not imply success.", + example: "Insufficient Fee" + } + }, + required: [:operation_id, :type, :status, :rollback, :timestamp], + additionalProperties: false + }) +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 9d6053bef92..a38d5ab33df 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,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do alias Explorer.Tags.AddressTag alias Plug.Conn.Query + @tac_operations_path "/api/v2/tac/operations" + describe "/search" do setup do initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) @@ -1236,9 +1238,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, + "type": "TON_TAC_TON", + "status": "success", + "rollback": false, "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "sender": null, + "error_reason": null } ], "next_page_params": null @@ -1248,7 +1253,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect_once( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -1263,9 +1268,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, + "type" => "TON_TAC_TON", + "status" => "success", + "rollback" => false, "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "sender" => nil, + "error_reason" => nil }, "type" => "tac_operation" } @@ -1293,17 +1301,61 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + # `next_page_params` is omitted rather than null: the v2 schema does not guarantee the key tac_response = """ { - "items": [], - "next_page_params": null + "items": [] } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, + 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" => [], + "next_page_params" => nil + } == json_response(request, 200) + end + + test "degrades gracefully on an unexpected body 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) + + 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" + + # 200 with a body that has no `items` key, e.g. a mispointed route or an error envelope + tac_response = """ + { + "message": "not found" + } + """ + + Bypass.expect( + bypass, + "GET", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -1344,9 +1396,10 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "type": "TAC_TON", + "status": "pending", + "rollback": false, + "timestamp": "2025-05-14T19:16:38.000Z" } ], "next_page_params": null @@ -1356,7 +1409,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -1371,9 +1424,10 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "type" => "TAC_TON", + "status" => "pending", + "rollback" => false, + "timestamp" => "2025-05-14T19:16:38.000Z" }, "type" => "tac_operation" } in tl(response["items"]) @@ -1413,9 +1467,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, + "type": "TON_TAC", + "status": "failed", + "rollback": false, "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "sender": null, + "error_reason": "Insufficient Fee" } ], "next_page_params": null @@ -1425,7 +1482,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -1440,9 +1497,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, + "type" => "TON_TAC", + "status" => "failed", + "rollback" => false, "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "sender" => nil, + "error_reason" => "Insufficient Fee" }, "type" => "tac_operation" } in tl(response["items"]) @@ -1484,15 +1544,21 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do #{for i <- 10..59, do: """ { "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" + "type": "TAC_TON", + "status": "success", + "rollback": false, + "timestamp": "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", + "sender": { + "address": "#{address_hash}", + "blockchain": "TAC" + }, + "error_reason": null }#{if i == 59, do: "", else: ","} """} ], "next_page_params": { "page_token": 1747250219, - "page_size": 50 + "page_items": 50 } } """ @@ -1503,15 +1569,21 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do #{for i <- 10..59, do: """ { "operation_id": "#{operation_id}", - "sender": "#{address_hash}", + "type": "TAC_TON", + "status": "success", + "rollback": false, "timestamp": "#{if i == 0, do: "2025-05-14T19:16:59.000Z", else: "2025-05-14T19:17:#{i}.000Z"}", - "type": "TON_TAC_TON" + "sender": { + "address": "#{address_hash}", + "blockchain": "TAC" + }, + "error_reason": null }#{if i == 59, do: "", else: ","} """} ], "next_page_params": { "page_token": 1747250279, - "page_size": 50 + "page_items": 50 } } """ @@ -1521,9 +1593,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": "#{address_hash}", + "type": "TAC_TON", + "status": "success", + "rollback": false, "timestamp": "2025-05-14T19:18:01.000Z", - "type": "TON_TAC_TON" + "sender": { + "address": "#{address_hash}", + "blockchain": "TAC" + }, + "error_reason": null } ], "next_page_params": null @@ -1533,7 +1611,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> case conn.params["page_token"] do nil -> Plug.Conn.resp(conn, 200, tac_first_response) @@ -1561,9 +1639,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => address_hash, + "type" => "TAC_TON", + "status" => "success", + "rollback" => false, "timestamp" => "2025-05-14T19:18:01.000Z", - "type" => "TON_TAC_TON" + "sender" => %{ + "address" => address_hash, + "blockchain" => "TAC" + }, + "error_reason" => nil }, "type" => "tac_operation" } @@ -1594,12 +1678,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "type": "ROLLBACK", + "type": "TON_TAC_TON", + "status": "failed", + "rollback": true, "timestamp": "2025-06-05T12:21:11.000Z", "sender": { "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", "blockchain": "TON" - } + }, + "error_reason": null } ], "next_page_params": null @@ -1609,7 +1696,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> case conn.params["q"] do expected_q @@ -1629,110 +1716,59 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end ) + expected_items = [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type" => "TON_TAC_TON", + "status" => "failed", + "rollback" => true, + "timestamp" => "2025-06-05T12:21:11.000Z", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "error_reason" => nil + }, + "type" => "tac_operation" + } + ] + 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"] + assert expected_items == 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"] + assert expected_items == 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"] + assert expected_items == 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"] + assert expected_items == 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"] + assert expected_items == response["items"] request = get( @@ -1742,21 +1778,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do 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"] + assert expected_items == response["items"] end test "finds TAC operations with transaction and paginates", %{conn: conn} do @@ -1786,15 +1808,18 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do #{for i <- 0..49, do: """ { "operation_id": "#{operation_id}", + "type": "TON_TAC_TON", + "status": "success", + "rollback": false, + "timestamp": "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", "sender": null, - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" + "error_reason": null }#{if i == 49, do: "", else: ","} """} ], "next_page_params": { "page_token": 1747250209, - "page_size": 50 + "page_items": 50 } } """ @@ -1804,9 +1829,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, + "type": "TON_TAC_TON", + "status": "success", + "rollback": false, "timestamp": "2025-05-14T19:16:50.000Z", - "type": "TON_TAC_TON" + "sender": null, + "error_reason": null } ], "next_page_params": null @@ -1816,7 +1844,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> case conn.params["page_token"] do nil -> Plug.Conn.resp(conn, 200, tac_first_response) @@ -1838,9 +1866,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, + "type" => "TON_TAC_TON", + "status" => "success", + "rollback" => false, "timestamp" => "2025-05-14T19:16:50.000Z", - "type" => "TON_TAC_TON" + "sender" => nil, + "error_reason" => nil }, "type" => "tac_operation" } @@ -2242,9 +2273,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, + "type": "TON_TAC_TON", + "status": "success", + "rollback": false, "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "sender": null, + "error_reason": null } ], "next_page_params": null @@ -2254,7 +2288,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -2268,9 +2302,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, + "type" => "TON_TAC_TON", + "status" => "success", + "rollback" => false, "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "sender" => nil, + "error_reason" => nil }, "type" => "tac_operation" } @@ -2303,9 +2340,10 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "type": "TAC_TON", + "status": "pending", + "rollback": false, + "timestamp": "2025-05-14T19:16:38.000Z" } ], "next_page_params": null @@ -2315,7 +2353,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -2330,9 +2368,10 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "type" => "TAC_TON", + "status" => "pending", + "rollback" => false, + "timestamp" => "2025-05-14T19:16:38.000Z" }, "type" => "tac_operation" } in tl(response) @@ -2372,9 +2411,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "#{operation_id}", - "sender": null, + "type": "TON_TAC", + "status": "failed", + "rollback": false, "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "sender": null, + "error_reason": "Insufficient Fee" } ], "next_page_params": null @@ -2384,7 +2426,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id Plug.Conn.resp(conn, 200, tac_response) @@ -2399,9 +2441,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, - "sender" => nil, + "type" => "TON_TAC", + "status" => "failed", + "rollback" => false, "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" + "sender" => nil, + "error_reason" => "Insufficient Fee" }, "type" => "tac_operation" } in tl(response) @@ -2444,15 +2489,21 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do #{for i <- 0..49, do: """ { "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" + "type": "TAC_TON", + "status": "success", + "rollback": false, + "timestamp": "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", + "sender": { + "address": "#{address_hash}", + "blockchain": "TAC" + }, + "error_reason": null }#{if i == 49, do: "", else: ","} """} ], "next_page_params": { "page_token": "2025-05-14T19:16:49.000Z", - "page_size": 50 + "page_items": 50 } } """ @@ -2460,7 +2511,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == address_hash @@ -2490,9 +2541,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest 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_TON", + "status" => "success", + "rollback" => false, + "timestamp" => "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", + "sender" => %{ + "address" => address_hash, + "blockchain" => "TAC" + }, + "error_reason" => nil }, "type" => "tac_operation" } @@ -2524,12 +2581,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "items": [ { "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "type": "ROLLBACK", + "type": "TON_TAC_TON", + "status": "failed", + "rollback": true, "timestamp": "2025-06-05T12:21:11.000Z", "sender": { "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", "blockchain": "TON" - } + }, + "error_reason": null } ], "next_page_params": null @@ -2539,7 +2599,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> case conn.params["q"] do expected_q @@ -2559,110 +2619,59 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end ) + expected_items = [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type" => "TON_TAC_TON", + "status" => "failed", + "rollback" => true, + "timestamp" => "2025-06-05T12:21:11.000Z", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "error_reason" => nil + }, + "type" => "tac_operation" + } + ] + 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 + assert expected_items == 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 + assert expected_items == 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 + assert expected_items == 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 + assert expected_items == 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 + assert expected_items == response request = get( @@ -2672,21 +2681,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do 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 + assert expected_items == response end test "finds a lot if TAC operations with transaction", %{conn: conn} do @@ -2718,15 +2713,18 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do #{for i <- 0..49, do: """ { "operation_id": "#{operation_id}", + "type": "TON_TAC_TON", + "status": "success", + "rollback": false, + "timestamp": "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", "sender": null, - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" + "error_reason": null }#{if i == 49, do: "", else: ","} """} ], "next_page_params": { "page_token": "2025-05-14T19:16:49.000Z", - "page_size": 50 + "page_items": 50 } } """ @@ -2734,7 +2732,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Bypass.expect( bypass, "GET", - "/api/v1/tac/operations", + @tac_operations_path, fn conn -> assert conn.params["q"] == operation_id @@ -2759,9 +2757,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "tac_operation" => %{ "operation_id" => operation_id, + "type" => "TON_TAC_TON", + "status" => "success", + "rollback" => false, + "timestamp" => "2025-05-14T19:16:#{String.pad_leading(Integer.to_string(i), 2, "0")}.000Z", "sender" => nil, - "timestamp" => "2025-05-14T19:16:#{i}.000Z", - "type" => "TON_TAC_TON" + "error_reason" => nil }, "type" => "tac_operation" } 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 fe3f3e6180c..cf2cebc97d7 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex @@ -24,8 +24,12 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do 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}} + {:ok, %{"items" => operations} = response} -> + {:ok, %{items: operations, next_page_params: Map.get(response, "next_page_params")}} + + {:ok, unexpected} -> + log_error({:unexpected_body, unexpected}) + {:error, @request_error_msg} error -> error @@ -46,6 +50,14 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do end {:ok, %{body: _body, status_code: 404}} -> + Logger.warning(fn -> + [ + "#{@request_error_msg}: ", + url, + " returned 404, TAC operations will be omitted from search results" + ] + end) + {:error, :not_found} {_, error} -> @@ -68,6 +80,6 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do end defp base_url do - "#{Microservice.base_url(__MODULE__)}/api/v1" + "#{Microservice.base_url(__MODULE__)}/api/v2" end end diff --git a/cspell.json b/cspell.json index e890a65a37c..008c6164dd9 100644 --- a/cspell.json +++ b/cspell.json @@ -435,6 +435,7 @@ "millis", "mintings", "misestimates", + "mispointed", "mistmatches", "miterlimit", "Mixfile", From bb0ff8a257f5dd11afde03d05a96c6997a54daf0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 20 Aug 2026 11:09:17 +0300 Subject: [PATCH 03/11] perf: batch on-demand internal transaction trace requests (#14724) --- .../fetcher/on_demand/internal_transaction.ex | 144 ++++++++++++------ .../on_demand/internal_transaction_test.exs | 136 +++++++++++++++++ config/runtime.exs | 6 +- 3 files changed, 241 insertions(+), 45 deletions(-) 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 ec606f2653e..045c2a0b448 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex @@ -18,6 +18,14 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do @default_paging_options %PagingOptions{page_size: 50} + # Limit how many trace requests are sent to the node in one JSON-RPC batch. + # Without them, all block/transaction trace calls of an on-demand fetch end up + # in a single batch (the transport-level ETHEREUM_JSONRPC_HTTP_BATCH_SIZE + # default of 500 is tuned for cheap calls, not traces), sharing one timeout + # window on the archive node. Batches are sent in parallel. + @default_blocks_batch_size 2 + @default_transactions_batch_size 20 + @doc """ Determines whether internal transactions should be fetched on-demand based on DB records and limit. @@ -569,57 +577,105 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do 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 + fetch_blocks_internal_transactions(block_numbers, json_rpc_named_arguments) + else + block_numbers + |> transactions_to_trace() + |> fetch_transactions_internal_transactions(json_rpc_named_arguments) + end + end + end - error -> - Logger.error( - "Failed to fetch internal transactions for blocks #{inspect(block_numbers)}: #{inspect(error)}" - ) + # Traces blocks in batches of `blocks_batch_size()` sent in parallel. Any + # failed batch fails the whole fetch (as a failed batch did before batching + # was introduced) to avoid silent gaps in paginated results. + defp fetch_blocks_internal_transactions(block_numbers, json_rpc_named_arguments) do + chunk_results = + block_numbers + |> Enum.chunk_every(blocks_batch_size()) + |> Task.async_stream( + fn chunk -> {chunk, EthereumJSONRPC.fetch_block_internal_transactions(chunk, json_rpc_named_arguments)} end, + timeout: :infinity + ) + |> Enum.map(fn {:ok, chunk_result} -> chunk_result end) - [] - end - else - Enum.reduce(block_numbers, [], fn block_number, acc_list -> - block_number - |> Transaction.get_transactions_of_block_number() - |> Transaction.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 + if Enum.all?(chunk_results, &match?({_chunk, {:ok, _}}, &1)) do + Enum.flat_map(chunk_results, fn {_chunk, {:ok, result}} -> result end) + else + Enum.each(chunk_results, fn + {_chunk, {:ok, _}} -> + :ok - error_or_ignore -> - Logger.error( - "Failed to fetch internal transactions for block #{block_number}: #{inspect(error_or_ignore)}" - ) + {chunk, error} -> + Logger.error("Failed to fetch internal transactions for blocks #{inspect(chunk)}: #{inspect(error)}") + end) - acc_list - end - end) - end + [] end end + # Collects traceable transactions of all the blocks with a single DB query, + # preserving the order of `block_numbers` (and transaction index order within + # a block). + defp transactions_to_trace(block_numbers) do + transactions_by_block_number = + block_numbers + |> Transaction.get_transactions_of_block_numbers() + |> Transaction.filter_non_traceable_transactions() + |> Enum.group_by(& &1.block_number) + + Enum.flat_map(block_numbers, fn block_number -> + transactions_by_block_number + |> Map.get(block_number, []) + |> Enum.sort_by(& &1.index) + |> Enum.map( + &%{ + block_number: &1.block_number, + hash_data: to_string(&1.hash), + transaction_index: &1.index + } + ) + end) + end + + # Traces transactions in cross-block batches of `transactions_batch_size()` + # sent in parallel, instead of one batch per block. A failed batch is skipped + # with a log, the same way a failed per-block batch was skipped before. + defp fetch_transactions_internal_transactions(transactions, json_rpc_named_arguments) do + transactions + |> Enum.chunk_every(transactions_batch_size()) + |> Task.async_stream( + fn chunk -> {chunk, do_fetch_transactions_internal_transactions(chunk, json_rpc_named_arguments)} end, + timeout: :infinity + ) + |> Enum.flat_map(fn + {:ok, {_chunk, {:ok, internal_transactions}}} -> + internal_transactions + + {:ok, {chunk, error_or_ignore}} -> + Logger.error( + "Failed to fetch internal transactions for transactions #{inspect(Enum.map(chunk, & &1.hash_data))}: #{inspect(error_or_ignore)}" + ) + + [] + end) + end + + defp do_fetch_transactions_internal_transactions(chunk, json_rpc_named_arguments) do + EthereumJSONRPC.fetch_internal_transactions(chunk, json_rpc_named_arguments) + catch + :exit, error -> + {:error, error, __STACKTRACE__} + end + + defp blocks_batch_size do + Application.get_env(:indexer, __MODULE__, [])[:blocks_batch_size] || @default_blocks_batch_size + end + + defp transactions_batch_size do + Application.get_env(:indexer, __MODULE__, [])[:transactions_batch_size] || @default_transactions_batch_size + end + defp internal_transactions_fetching_disabled? do Application.get_env(:indexer, __MODULE__, [])[:disabled?] == true 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 index 856ee8c762b..35cb34a4634 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 @@ -400,6 +400,142 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransactionTest do assert result |> Enum.filter(&(&1.block_number == 2)) |> Enum.count() == 1 end + test "fetch_by_address/2 chunks block trace requests by blocks_batch_size" do + Application.put_env(:indexer, Indexer.Fetcher.OnDemand.InternalTransaction, blocks_batch_size: 1) + + address = insert(:address) + address_hash_str = to_string(address.hash) + id_to_hash = insert(:address_id_to_address_hash, address: address) + + for block_number <- [1, 2] do + insert(:deleted_internal_transactions_address_placeholder, + address_id: id_to_hash.address_id, + block_number: block_number, + count_tos: 1, + count_froms: 1 + ) + end + + # with batch_size 1, blocks 2 and 1 must arrive as two single-request batches + expect(EthereumJSONRPC.Mox, :json_rpc, 2, fn [%{id: id, params: [quantity, _]}], _ -> + assert quantity in ["0x1", "0x2"] + + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => "0x", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => "0x", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + } + ] + } + ]} + 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: 2} + ] + + assert [%InternalTransaction{block_number: 2}, %InternalTransaction{block_number: 1}] = + InternalTransactionOnDemand.fetch_by_address(address.hash, opts) + end + + test "fetch_by_address/2 batches transaction trace requests across blocks" do + address = insert(:address) + address_hash_str = to_string(address.hash) + id_to_hash = insert(:address_id_to_address_hash, address: address) + + transactions = + for block_number <- [1, 2] do + insert(:deleted_internal_transactions_address_placeholder, + address_id: id_to_hash.address_id, + block_number: block_number, + count_tos: 1, + count_froms: 1 + ) + + block = insert(:block, number: block_number) + :transaction |> insert() |> with_block(block) + end + + transaction_hashes = MapSet.new(transactions, &to_string(&1.hash)) + + # transactions of both blocks must arrive in one cross-block batch + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn requests, _ -> + assert length(requests) == 2 + assert MapSet.new(requests, fn %{params: [hash, _]} -> hash end) == transaction_hashes + + {:ok, + Enum.map(requests, fn %{id: id} -> + %{ + id: id, + result: %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => "0x", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => "0x", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + } + end)} + end) + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, + tracer: "call_tracer", + debug_trace_timeout: "5s", + block_traceable?: false + ) + + opts = [ + direction: :to_address_hash, + paging_options: %PagingOptions{page_size: 2} + ] + + assert [%InternalTransaction{block_number: 2}, %InternalTransaction{block_number: 1}] = + InternalTransactionOnDemand.fetch_by_address(address.hash, opts) + end + test "fetch_by_address/2 (no suitable placeholders)" do address = insert(:address) address_hash_str = to_string(address.hash) diff --git a/config/runtime.exs b/config/runtime.exs index 8e3fe03df05..6a21f45185b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1201,7 +1201,11 @@ config :indexer, Indexer.Fetcher.InternalTransaction, disabled?: trace_url_missing? or ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER") config :indexer, Indexer.Fetcher.OnDemand.InternalTransaction, - disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER") + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER"), + blocks_batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_ON_DEMAND_INTERNAL_TRANSACTIONS_BLOCKS_BATCH_SIZE", 2, min: 1), + transactions_batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_ON_DEMAND_INTERNAL_TRANSACTIONS_TRANSACTIONS_BATCH_SIZE", 20, min: 1) disable_coin_balances_fetcher? = ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER") From 5b15ab002405fb9f259c5c6819649989ffcf790e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 20 Aug 2026 11:11:26 +0300 Subject: [PATCH 04/11] feat: add market source requests metric with endpoint type labels (#14725) --- apps/explorer/lib/explorer/market/AGENTS.md | 21 +++- apps/explorer/lib/explorer/market/source.ex | 41 +++++++- .../lib/explorer/market/source/coin_gecko.ex | 20 +++- .../explorer/market/source/coin_market_cap.ex | 12 ++- .../explorer/market/source/crypto_compare.ex | 4 +- .../lib/explorer/market/source/crypto_rank.ex | 15 ++- .../lib/explorer/market/source/defillama.ex | 4 +- .../lib/explorer/market/source/dia.ex | 16 +++- .../lib/explorer/market/source/mobula.ex | 12 ++- .../lib/explorer/prometheus/instrumenter.ex | 23 +++++ .../explorer/market/source_metrics_test.exs | 96 +++++++++++++++++++ 11 files changed, 240 insertions(+), 24 deletions(-) create mode 100644 apps/explorer/test/explorer/market/source_metrics_test.exs diff --git a/apps/explorer/lib/explorer/market/AGENTS.md b/apps/explorer/lib/explorer/market/AGENTS.md index b5a0c5ed418..8cdc2b93d03 100644 --- a/apps/explorer/lib/explorer/market/AGENTS.md +++ b/apps/explorer/lib/explorer/market/AGENTS.md @@ -10,4 +10,23 @@ on_exit(fn -> :persistent_term.put(:market_token_fetcher_enabled, false) end) -``` \ No newline at end of file +``` + +## Market source request metric + +All market source requests go through `Explorer.Market.Source.http_request/4`, which increments the +`market_source_requests_count` counter (`Explorer.Prometheus.Instrumenter`, exposed on `/metrics`) +with the `source`, `endpoint` and `status` labels. + +When adding a request to a market source, pass `__MODULE__` and a new endpoint atom: + +```elixir +Source.http_request(url, headers(), __MODULE__, :coins_details) +``` + +The endpoint atom is the *endpoint type*, not the URL: requests differing only in variable parts +(token address hash, coin id, pagination offset, date range) must share one atom, so that e.g. all +per-token DIA calls land in a single `asset_quotation_token` series. Conversely, when one path serves +two purposes, use two atoms (`coins_market_chart_price` vs `coins_market_chart_market_cap`) so the +driving fetcher is distinguishable. Never interpolate a variable into the atom — that would blow up +the metric cardinality. \ No newline at end of file diff --git a/apps/explorer/lib/explorer/market/source.ex b/apps/explorer/lib/explorer/market/source.ex index 2df47cf104d..7747ccf7f8b 100644 --- a/apps/explorer/lib/explorer/market/source.ex +++ b/apps/explorer/lib/explorer/market/source.ex @@ -48,6 +48,7 @@ defmodule Explorer.Market.Source do } alias Explorer.Market.Token + alias Explorer.Prometheus.Instrumenter # Native coin processing @callback native_coin_fetching_enabled?() :: boolean() | :ignore @@ -113,10 +114,18 @@ defmodule Explorer.Market.Source do handling for NFT-related responses. Error responses are formatted into descriptive error messages. + Every request is counted in the `market_source_requests_count` metric, labeled with + the source, the endpoint type and the request outcome. + ## Parameters - `source_url`: The URL to send the GET request to - `additional_headers`: Extra HTTP headers to be added to the default JSON content-type header + - `source_module`: The module of the market data source performing the request, used + as the `source` label of the metric + - `endpoint`: The endpoint type of the request, used as the `endpoint` label of the + metric. Requests to the same endpoint with different variable parts (token address + hash, coin id, pagination offset, etc.) must share the same endpoint type ## Returns - `{:ok, decoded_data}` if the request succeeds with status 200 and valid JSON @@ -128,9 +137,18 @@ 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(), [{atom() | binary(), binary()}]) :: {:ok, any()} | {:error, any()} - def http_request(source_url, additional_headers) do - case HttpClient.get(source_url, headers() ++ additional_headers) do + @spec http_request(String.t(), [{atom() | binary(), binary()}], module(), atom()) :: + {:ok, any()} | {:error, any()} + def http_request(source_url, additional_headers, source_module, endpoint) do + response = HttpClient.get(source_url, headers() ++ additional_headers) + + Instrumenter.market_source_request(source_label(source_module), endpoint, request_status(response)) + + handle_http_response(response) + end + + defp handle_http_response(response) do + case response do {:ok, %{body: body, status_code: 200}} -> parse_http_success_response(body) @@ -148,6 +166,23 @@ defmodule Explorer.Market.Source do end end + # Converts a source module into the `source` label of the + # `market_source_requests_count` metric, e.g. `Explorer.Market.Source.CoinGecko` + # becomes `"coin_gecko"`. + @spec source_label(module()) :: String.t() + defp source_label(source_module) do + source_module |> Module.split() |> List.last() |> Macro.underscore() + end + + # Converts an HTTP response into the `status` label of the + # `market_source_requests_count` metric. Error reasons of failed requests are not + # used as label values since they are unbounded. + @spec request_status({:ok, map()} | {:error, any()}) :: String.t() + defp request_status({:ok, %{status_code: 200}}), do: "ok" + defp request_status({:ok, %{status_code: status_code}}) when status_code in 300..526, do: to_string(status_code) + defp request_status({:ok, %{status_code: _status_code}}), do: "unexpected_status" + defp request_status({:error, _reason}), do: "transport_error" + defp parse_http_success_response(body) do case Helper.decode_json(body, true) do {:error, _reason} = error -> error diff --git a/apps/explorer/lib/explorer/market/source/coin_gecko.ex b/apps/explorer/lib/explorer/market/source/coin_gecko.ex index 581378b91c7..26109f6198c 100644 --- a/apps/explorer/lib/explorer/market/source/coin_gecko.ex +++ b/apps/explorer/lib/explorer/market/source/coin_gecko.ex @@ -53,7 +53,9 @@ defmodule Explorer.Market.Source.CoinGecko do |> URI.append_query("include_24hr_vol=true") |> URI.append_query("ids=#{joined_token_ids}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :simple_price ) do {:ok, data} -> to_import = put_market_data_to_tokens(to_fetch, data) @@ -89,7 +91,9 @@ defmodule Explorer.Market.Source.CoinGecko do |> URI.append_query("vs_currency=#{config(:currency)}") |> URI.append_query("days=#{previous_days}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :coins_market_chart_market_cap ) do market_caps = case market_caps_dates do @@ -133,7 +137,9 @@ defmodule Explorer.Market.Source.CoinGecko do |> URI.append_query("developer_data=false") |> URI.append_query("sparkline=false") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :coins_details ) do {:ok, %Token{ @@ -166,7 +172,9 @@ defmodule Explorer.Market.Source.CoinGecko do |> URI.append_path("/coins/list") |> URI.append_query("include_platform=true") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :coins_list ) do tokens |> Enum.reduce([], &reduce_coingecko_token(&1, &2, platform)) @@ -247,7 +255,9 @@ defmodule Explorer.Market.Source.CoinGecko do |> URI.append_query("vs_currency=#{config(:currency)}") |> URI.append_query("days=#{previous_days}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :coins_market_chart_price ) do closings = case prices do diff --git a/apps/explorer/lib/explorer/market/source/coin_market_cap.ex b/apps/explorer/lib/explorer/market/source/coin_market_cap.ex index 8dc6a8f4ed3..bef7c735d10 100644 --- a/apps/explorer/lib/explorer/market/source/coin_market_cap.ex +++ b/apps/explorer/lib/explorer/market/source/coin_market_cap.ex @@ -57,7 +57,9 @@ defmodule Explorer.Market.Source.CoinMarketCap do |> URI.append_query("convert_id=#{currency_id}") |> URI.append_query("aux=market_cap") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :cryptocurrency_quotes_historical_market_cap ) do quotes = market_data["quotes"] @@ -101,7 +103,9 @@ defmodule Explorer.Market.Source.CoinMarketCap do |> URI.append_query("convert_id=#{convert_id}") |> URI.append_query("aux=circulating_supply,total_supply") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :cryptocurrency_quotes_latest ) do token_properties = market_data |> Map.values() |> List.first() || %{} currency_id = token_properties["quote"][config(:currency_id)] @@ -150,7 +154,9 @@ defmodule Explorer.Market.Source.CoinMarketCap do |> URI.append_query("convert_id=#{currency_id}") |> URI.append_query("aux=price") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :cryptocurrency_quotes_historical_price ) do closing_quotes = case quotes do diff --git a/apps/explorer/lib/explorer/market/source/crypto_compare.ex b/apps/explorer/lib/explorer/market/source/crypto_compare.ex index b2115a8a9d4..0b7cc0e9801 100644 --- a/apps/explorer/lib/explorer/market/source/crypto_compare.ex +++ b/apps/explorer/lib/explorer/market/source/crypto_compare.ex @@ -65,7 +65,9 @@ defmodule Explorer.Market.Source.CryptoCompare do |> URI.append_query("tsym=#{config(:currency)}") |> URI.append_query("extraParams=Blockscout/#{Application.spec(:explorer)[:vsn]}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :data_histoday ) do result = for item <- data do diff --git a/apps/explorer/lib/explorer/market/source/crypto_rank.ex b/apps/explorer/lib/explorer/market/source/crypto_rank.ex index 3a22bc691c6..190e9c444dc 100644 --- a/apps/explorer/lib/explorer/market/source/crypto_rank.ex +++ b/apps/explorer/lib/explorer/market/source/crypto_rank.ex @@ -37,7 +37,9 @@ defmodule Explorer.Market.Source.CryptoRank do |> URI.append_query("limit=#{batch_size}") |> URI.append_query("skip=#{skip}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :currencies_contracts ) do {tokens_to_import, initial_tokens_len} = tokens |> Enum.reduce({[], 0}, &reduce_token(platform_id, &1, &2)) @@ -114,7 +116,12 @@ defmodule Explorer.Market.Source.CryptoRank do defp do_fetch_coin(coin_id, coin_id_not_specified_error) do with coin_id when not is_nil(coin_id) <- coin_id, {:ok, %{"data" => coin}} <- - Source.http_request(base_url() |> URI.append_path("/currencies/#{coin_id}") |> URI.to_string(), headers()) do + Source.http_request( + base_url() |> URI.append_path("/currencies/#{coin_id}") |> URI.to_string(), + headers(), + __MODULE__, + :currencies_details + ) do coin_data = coin["values"][config(:currency)] {:ok, @@ -152,7 +159,9 @@ defmodule Explorer.Market.Source.CryptoRank do |> URI.append_query("from=#{from}") |> URI.append_query("to=#{to}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :currencies_sparkline ) do closing_prices = case opening_prices do diff --git a/apps/explorer/lib/explorer/market/source/defillama.ex b/apps/explorer/lib/explorer/market/source/defillama.ex index a740ee7e5dc..19f4325743b 100644 --- a/apps/explorer/lib/explorer/market/source/defillama.ex +++ b/apps/explorer/lib/explorer/market/source/defillama.ex @@ -54,7 +54,9 @@ defmodule Explorer.Market.Source.DefiLlama do {:ok, data} when is_list(data) <- Source.http_request( base_url() |> URI.append_path("/historicalChainTvl/#{URI.encode(coin_id)}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :historical_chain_tvl ) do result = Enum.map(data, fn %{"date" => date, "tvl" => tvl} -> diff --git a/apps/explorer/lib/explorer/market/source/dia.ex b/apps/explorer/lib/explorer/market/source/dia.ex index ee98d269390..d8c0ad47e76 100644 --- a/apps/explorer/lib/explorer/market/source/dia.ex +++ b/apps/explorer/lib/explorer/market/source/dia.ex @@ -58,7 +58,9 @@ defmodule Explorer.Market.Source.DIA do |> URI.append_path("/#{blockchain}") |> URI.append_path("/#{token.contract_address_hash}") |> URI.to_string(), - [] + [], + __MODULE__, + :asset_quotation_token ) do {:ok, data} -> token_to_import = @@ -142,7 +144,9 @@ defmodule Explorer.Market.Source.DIA do base_url() |> URI.append_path("/assetQuotation/#{blockchain}/#{coin_address_hash}") |> URI.to_string(), - [] + [], + __MODULE__, + :asset_quotation_coin ) do {:ok, %Token{ @@ -176,7 +180,9 @@ defmodule Explorer.Market.Source.DIA do |> URI.append_path("/quotedAssets") |> URI.append_query("blockchain=#{blockchain}") |> URI.to_string(), - [] + [], + __MODULE__, + :quoted_assets ) do tokens |> Enum.reduce([], &reduce_dia_token(&1, &2, coin_address_hash)) @@ -234,7 +240,9 @@ defmodule Explorer.Market.Source.DIA do |> URI.append_query("starttime=#{unix_from}") |> URI.append_query("endtime=#{unix_now}") |> URI.to_string(), - [] + [], + __MODULE__, + :asset_chart_points ) do values |> Enum.reduce_while(%{}, fn value, acc -> diff --git a/apps/explorer/lib/explorer/market/source/mobula.ex b/apps/explorer/lib/explorer/market/source/mobula.ex index b5bdbe7f21f..c605ea11d33 100644 --- a/apps/explorer/lib/explorer/market/source/mobula.ex +++ b/apps/explorer/lib/explorer/market/source/mobula.ex @@ -41,7 +41,9 @@ defmodule Explorer.Market.Source.Mobula do |> URI.append_query("limit=#{batch_size}") |> URI.append_query("offset=#{offset}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :market_query ) do {tokens_to_import, initial_tokens_len} = Enum.reduce(tokens, {[], 0}, &reduce_mobula_token/2) @@ -115,7 +117,9 @@ defmodule Explorer.Market.Source.Mobula do |> URI.append_path("/market/data") |> URI.append_query("asset=#{coin_id}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :market_data ) do {:ok, %Token{ @@ -155,7 +159,9 @@ defmodule Explorer.Market.Source.Mobula do |> URI.append_query("asset=#{coin_id}") |> URI.append_query("from=#{timestamp_ms}") |> URI.to_string(), - headers() + headers(), + __MODULE__, + :market_history ) do result = for [date_ms, price] <- price_history do diff --git a/apps/explorer/lib/explorer/prometheus/instrumenter.ex b/apps/explorer/lib/explorer/prometheus/instrumenter.ex index 021d9841765..b3aaebc6f8b 100644 --- a/apps/explorer/lib/explorer/prometheus/instrumenter.ex +++ b/apps/explorer/lib/explorer/prometheus/instrumenter.ex @@ -88,6 +88,14 @@ defmodule Explorer.Prometheus.Instrumenter do registry: :public ] + # metrics of market data sources + + @counter [ + name: :market_source_requests_count, + labels: [:source, :endpoint, :status], + help: "Number of HTTP requests sent to market data sources by source, endpoint type and outcome" + ] + @gauge [name: :average_block_time, help: "Average block time in milliseconds"] @gauge [name: :batch_average_time, help: "L2 average batch time"] @@ -220,6 +228,21 @@ defmodule Explorer.Prometheus.Instrumenter do Counter.inc(name: :failed_uploading_media_number, registry: :public) end + @doc """ + Increments the counter of HTTP requests sent to a market data source. + + ## Parameters + - `source`: The market data source name, e.g. `"coin_gecko"` + - `endpoint`: The endpoint type of the request. Requests to the same endpoint with + different variable parts (token address hash, coin id, pagination offset, etc.) + share the same endpoint type + - `status`: The outcome of the request, e.g. `"ok"` or `"429"` + """ + @spec market_source_request(String.t(), atom(), String.t()) :: :ok + def market_source_request(source, endpoint, status) do + Counter.inc(name: :market_source_requests_count, labels: [source, endpoint, status]) + end + @doc """ Defines the metric for the average block time in milliseconds. """ diff --git a/apps/explorer/test/explorer/market/source_metrics_test.exs b/apps/explorer/test/explorer/market/source_metrics_test.exs new file mode 100644 index 00000000000..6031ebaea64 --- /dev/null +++ b/apps/explorer/test/explorer/market/source_metrics_test.exs @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: LicenseRef-Blockscout +defmodule Explorer.Market.SourceMetricsTest do + use ExUnit.Case + + use Prometheus.Metric + + alias Explorer.Market.Source + alias Explorer.Market.Source.CoinGecko + alias Plug.Conn + + setup do + bypass = Bypass.open() + + initial_tesla_adapter = Application.fetch_env(:tesla, :adapter) + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + case initial_tesla_adapter do + {:ok, adapter} -> Application.put_env(:tesla, :adapter, adapter) + :error -> Application.delete_env(:tesla, :adapter) + end + end) + + {:ok, bypass: bypass} + end + + describe "market_source_requests_count metric" do + test "counts a successful request with the source and endpoint labels", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/test", fn conn -> + Conn.resp(conn, 200, ~s({"result": "ok"})) + end) + + labels = ["coin_gecko", :test_endpoint, "ok"] + value_before = counter_value(labels) + + assert {:ok, %{"result" => "ok"}} = + Source.http_request("http://localhost:#{bypass.port}/test", [], CoinGecko, :test_endpoint) + + assert counter_value(labels) == value_before + 1 + end + + test "counts requests to the same endpoint with different variable parts in one series", %{bypass: bypass} do + Bypass.expect(bypass, "GET", "/test/0x1", fn conn -> Conn.resp(conn, 200, ~s({})) end) + Bypass.expect(bypass, "GET", "/test/0x2", fn conn -> Conn.resp(conn, 200, ~s({})) end) + + labels = ["dia", :test_shared_endpoint, "ok"] + value_before = counter_value(labels) + + Enum.each(["0x1", "0x2"], fn address_hash -> + assert {:ok, _} = + Source.http_request( + "http://localhost:#{bypass.port}/test/#{address_hash}", + [], + Source.DIA, + :test_shared_endpoint + ) + end) + + assert counter_value(labels) == value_before + 2 + end + + test "counts an error response with the status code as the status label", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/test", fn conn -> + Conn.resp(conn, 429, "Too many requests") + end) + + labels = ["coin_gecko", :test_endpoint, "429"] + value_before = counter_value(labels) + + assert {:error, "429: Too many requests"} = + Source.http_request("http://localhost:#{bypass.port}/test", [], CoinGecko, :test_endpoint) + + assert counter_value(labels) == value_before + 1 + end + + test "counts a transport error", %{bypass: bypass} do + Bypass.down(bypass) + + labels = ["coin_gecko", :test_endpoint, "transport_error"] + value_before = counter_value(labels) + + assert {:error, _reason} = + Source.http_request("http://localhost:#{bypass.port}/test", [], CoinGecko, :test_endpoint) + + assert counter_value(labels) == value_before + 1 + end + end + + defp counter_value(labels) do + case Counter.value(name: :market_source_requests_count, labels: labels) do + :undefined -> 0 + value -> value + end + end +end From 86092e0cd88724945e3d64c0c0e084098f72be22 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 20 Aug 2026 20:50:49 +0300 Subject: [PATCH 05/11] feat: add missing address native coin balances count indexer metric (#14729) --- .../chain/metrics/queries/indexer_metrics.ex | 57 +++++++++++++++++++ .../chain/metrics/indexer_metrics_test.exs | 43 ++++++++++++++ .../lib/indexer/prometheus/instrumenter.ex | 8 +++ config/runtime.exs | 4 +- docker-compose/envs/common-blockscout.env | 1 + 5 files changed, 112 insertions(+), 1 deletion(-) 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 f7fb9b37b9c..cfa2ba93750 100644 --- a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex +++ b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex @@ -224,6 +224,63 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do end end + @doc """ + Query to get the count of address native coin balances with missing values + """ + # sobelow_skip ["SQL"] + @spec missing_address_native_coin_balances_count() :: integer() + def missing_address_native_coin_balances_count do + block_ranges = RangesHelper.get_block_ranges() + + if block_ranges == [] do + 0 + else + {range_conditions, params} = + Enum.reduce(block_ranges, {[], []}, fn + first..last//_, {conditions, acc_params} -> + from = min(first, last) + to = max(first, last) + param_index_from = length(acc_params) + 1 + param_index_to = length(acc_params) + 2 + + condition = + "(cb.block_number >= $#{param_index_from}::bigint AND cb.block_number <= $#{param_index_to}::bigint)" + + {[condition | conditions], [to, from | acc_params]} + + start_from, {conditions, acc_params} -> + param_index = length(acc_params) + 1 + condition = "cb.block_number >= $#{param_index}::bigint" + {[condition | conditions], [start_from | acc_params]} + end) + + range_filter = + range_conditions + |> Enum.reverse() + |> Enum.join(" OR ") + + sql_string = """ + SELECT COUNT(1) as missing_address_native_coin_balances_count + FROM address_coin_balances cb + WHERE cb.value_fetched_at is NULL + AND (#{range_filter}); + """ + + case SQL.query(Repo, sql_string, Enum.reverse(params), timeout: :infinity) do + {:ok, + %Postgrex.Result{ + command: :select, + columns: ["missing_address_native_coin_balances_count"], + rows: [[missing_address_native_coin_balances_count]] + }} -> + missing_address_native_coin_balances_count + + _ -> + 0 + end + end + end + @doc """ Query to get the count of archival token balances with missing values """ diff --git a/apps/explorer/test/explorer/chain/metrics/indexer_metrics_test.exs b/apps/explorer/test/explorer/chain/metrics/indexer_metrics_test.exs index c957f246f1c..622e66d5ec8 100644 --- a/apps/explorer/test/explorer/chain/metrics/indexer_metrics_test.exs +++ b/apps/explorer/test/explorer/chain/metrics/indexer_metrics_test.exs @@ -145,6 +145,49 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetricsTest do end end + describe "missing_address_native_coin_balances_count/0" do + test "counts only unfetched coin balances within configured ranges and latest tail" do + previous_block_ranges = Application.get_env(:indexer, :block_ranges) + on_exit(fn -> Application.put_env(:indexer, :block_ranges, previous_block_ranges) end) + + Application.put_env(:indexer, :block_ranges, "1..3,5..latest") + + address = insert(:address) + + # within ranges and unfetched -> counted + insert(:unfetched_balance, address_hash: address.hash, block_number: 1) + insert(:unfetched_balance, address_hash: address.hash, block_number: 7) + # within ranges but fetched -> not counted + insert(:unfetched_balance, + address_hash: address.hash, + block_number: 2, + value: 100, + value_fetched_at: DateTime.utc_now() + ) + + # unfetched but outside the ranges -> not counted + insert(:unfetched_balance, address_hash: address.hash, block_number: 4) + + assert IndexerMetrics.missing_address_native_coin_balances_count() == 2 + end + + test "counts only within finite ranges" do + previous_block_ranges = Application.get_env(:indexer, :block_ranges) + on_exit(fn -> Application.put_env(:indexer, :block_ranges, previous_block_ranges) end) + + Application.put_env(:indexer, :block_ranges, "10..12,20..22") + + address = insert(:address) + + insert(:unfetched_balance, address_hash: address.hash, block_number: 11) + insert(:unfetched_balance, address_hash: address.hash, block_number: 21) + # outside the finite ranges -> not counted + insert(:unfetched_balance, address_hash: address.hash, block_number: 30) + + assert IndexerMetrics.missing_address_native_coin_balances_count() == 2 + end + end + describe "missing_archival_token_balances_count/0" do test "returns 0 when archival token balances fetcher is disabled" do previous_config = diff --git a/apps/indexer/lib/indexer/prometheus/instrumenter.ex b/apps/indexer/lib/indexer/prometheus/instrumenter.ex index cc77c90eb40..0207280db0f 100644 --- a/apps/indexer/lib/indexer/prometheus/instrumenter.ex +++ b/apps/indexer/lib/indexer/prometheus/instrumenter.ex @@ -60,6 +60,7 @@ defmodule Indexer.Prometheus.Instrumenter do ] @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: :missing_address_native_coin_balances_count, help: "Number of missing address native coin balances"] @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"] @@ -244,6 +245,13 @@ defmodule Indexer.Prometheus.Instrumenter do 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 address native coin balances. + """ + @spec missing_address_native_coin_balances_count(integer()) :: :ok + def missing_address_native_coin_balances_count(value), + do: Gauge.set([name: :missing_address_native_coin_balances_count], value) + @doc """ Defines the metric for the number of missing token balances in history. """ diff --git a/config/runtime.exs b/config/runtime.exs index 6a21f45185b..c7db8f4a3ce 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1756,7 +1756,9 @@ config :indexer, Indexer.Prometheus.Metrics, 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") + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT", "true"), + missing_address_native_coin_balances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_ADDRESS_NATIVE_COIN_BALANCES_COUNT", "true") } config :indexer, Indexer.Prometheus.RealtimeMetrics, diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index b446302e056..b0b69cf8b90 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -652,6 +652,7 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT= # INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT= # INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT= +# INDEXER_METRICS_ENABLED_MISSING_ADDRESS_NATIVE_COIN_BALANCES_COUNT= # INDEXER_REALTIME_METRICS_ENABLED= # CSV_EXPORT_LIMIT= # CSV_EXPORT_ASYNC_ENABLED= From d7dbed5eac65dbb2b7023ba280b1ff21ef8c7a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:11:13 +0300 Subject: [PATCH 06/11] chore(deps): bump tesla from 1.21.0 to 1.21.2 (#14736) 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 fd8bb90d038..c441b375c54 100644 --- a/mix.lock +++ b/mix.lock @@ -171,7 +171,7 @@ "statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, - "tesla": {:hex, :tesla, "1.21.0", "de3dc7b0ddbbd72a2fdab02decc977c0fa910aa04cbfb92aaca5e93a839ad2a5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, 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 or >= 4.0.2 and < 5.0.0-0", [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]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, 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", "1a0b8c9a7c3676beafd33db5e13f5e486af936c1a9a9f56b037e7861d916d333"}, + "tesla": {:hex, :tesla, "1.21.2", "71b3a8a000802fa82f0b87c22acbca1e8f6a03f23d727e01b66687111681debc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, 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 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.5.0", [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]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, 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", "296bbafd1a328533c57e6b0e93078983cf40a827546e58a20464ce75485d5254"}, "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 d341c37a632d0bfd4a09c6ea28075ba181ba1761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:11:36 +0300 Subject: [PATCH 07/11] chore(deps): bump phoenix_live_view from 1.2.9 to 1.2.10 (#14737) 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 c441b375c54..cf265a01644 100644 --- a/mix.lock +++ b/mix.lock @@ -136,7 +136,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.2.9", "d35ddac2fab4480e6bdbf712ff104fd9e969a2bbe81b578ee80f066b371f56db", [: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", [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", "2f9528c3d7046edabbb30a91710ca33988f8d8bc20a964a1fc48b32134572afa"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.10", "eb4958045f71d4962373e9ed5967b592375a9dafc73f52997f3996759998a39d", [: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", [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", "bcf9d64846b770bc64b1a58dc40af406d80eb61b6e516f7aea4cc8cc15e0a1d9"}, "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 b81e39bd6db3cc0240758e870ad807de775328d1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 25 Aug 2026 13:06:03 +0300 Subject: [PATCH 08/11] fix: use cgroup memory limit for memory monitor in containers (#14739) --- apps/indexer/lib/indexer/memory/monitor.ex | 43 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/memory/monitor.ex b/apps/indexer/lib/indexer/memory/monitor.ex index ee77f44ca56..05e634bf12c 100644 --- a/apps/indexer/lib/indexer/memory/monitor.ex +++ b/apps/indexer/lib/indexer/memory/monitor.ex @@ -111,12 +111,53 @@ defmodule Indexer.Memory.Monitor do _ -> Application.get_env(:indexer, :system_memory_percentage) end - case :memsup.get_system_memory_data()[:total_memory] do + case total_memory() do nil -> default_limit total_memory -> floor(total_memory * percentage / 100) end end + @cgroup_memory_limit_paths [ + # cgroup v2 + "/sys/fs/cgroup/memory.max", + # cgroup v1 + "/sys/fs/cgroup/memory/memory.limit_in_bytes" + ] + + # cgroup v1 reports "no limit" as a huge sentinel number (PAGE_COUNTER_MAX) + # rather than a keyword, so values above this threshold are treated as unset + @cgroup_no_limit_threshold 1 <<< 60 + + # When running in a container, the memory available to the pod is defined by + # its cgroup limit, while `:memsup` reports the host's total memory, so the + # cgroup limit takes precedence when it is present and set. + defp total_memory do + cgroup_memory_limit() || :memsup.get_system_memory_data()[:total_memory] + end + + defp cgroup_memory_limit do + Enum.find_value(@cgroup_memory_limit_paths, &read_cgroup_memory_limit/1) + end + + defp read_cgroup_memory_limit(path) do + case File.read(path) do + {:ok, content} -> parse_cgroup_memory_limit(content) + _ -> nil + end + end + + @doc false + def parse_cgroup_memory_limit(content) do + with {limit, ""} <- Integer.parse(String.trim(content)), + true <- limit > 0 and limit < @cgroup_no_limit_threshold do + limit + else + # content is "max" (cgroup v2 for "no limit"), not a plain positive integer, + # or the limit is not set + _ -> nil + end + end + defp flush(message) do receive do ^message -> flush(message) From b822f531af321651387fa93ff11ccb3781dded86 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 25 Aug 2026 14:17:58 +0300 Subject: [PATCH 09/11] Add extra indexer monitor test suite --- .../test/indexer/memory/monitor_test.exs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 apps/indexer/test/indexer/memory/monitor_test.exs diff --git a/apps/indexer/test/indexer/memory/monitor_test.exs b/apps/indexer/test/indexer/memory/monitor_test.exs new file mode 100644 index 00000000000..5ebbd130555 --- /dev/null +++ b/apps/indexer/test/indexer/memory/monitor_test.exs @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LicenseRef-Blockscout +defmodule Indexer.Memory.MonitorTest do + use ExUnit.Case, async: true + + alias Indexer.Memory.Monitor + + describe "parse_cgroup_memory_limit/1" do + test "parses a valid numeric limit" do + assert Monitor.parse_cgroup_memory_limit("8589934592\n") == 8_589_934_592 + end + + test "returns nil for \"max\" (cgroup v2 \"no limit\")" do + assert Monitor.parse_cgroup_memory_limit("max\n") == nil + end + + test "returns nil for partially numeric content" do + assert Monitor.parse_cgroup_memory_limit("123garbage") == nil + end + + test "returns nil for a zero limit" do + assert Monitor.parse_cgroup_memory_limit("0\n") == nil + end + + test "returns nil for the cgroup v1 \"no limit\" sentinel" do + assert Monitor.parse_cgroup_memory_limit("9223372036854771712\n") == nil + end + end +end From 0da9169f8723c6971707ec2e9c58619933a17e01 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 26 Aug 2026 12:24:37 +0300 Subject: [PATCH 10/11] v11.2.8 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eden.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-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-eden.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 +- ...ocker-image-for-optimism-exeperimental.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-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eden.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-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 | 20 +++++++++++++++++++ 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 +- 57 files changed, 76 insertions(+), 56 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 02661c1fc27..867b724072c 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: 11.2.7 + RELEASE_VERSION: 11.2.8 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index ae392592858..c634c0a9c9e 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 34e069b4c8f..7c367402fb4 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: 11.2.7 + RELEASE_VERSION: 11.2.8 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eden.yml b/.github/workflows/pre-release-eden.yml index 5bb08e1e587..dbe00a95596 100644 --- a/.github/workflows/pre-release-eden.yml +++ b/.github/workflows/pre-release-eden.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 7bc322a63ed..e2dfa5c79b9 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 3fd0f7da244..87a39303e5f 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 c147575e6c7..9c9b9a28599 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 5ea8c66333a..c79c0c0f405 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 a369b6892c5..3cf6e91c009 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 dc0199f8e0d..aaf92953cbc 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 1023db45512..f0dec4d685a 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 3983e8dc1a2..f553925c55f 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 568109ec61b..a07fd848e32 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 409e788bb05..c88c8e00ed6 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 1c57a408310..1b28ab91041 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 a8019a3d4df..afe5d319374 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 11.2.7 + RELEASE_VERSION: 11.2.8 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 0bb488771eb..8aa2cc5d23a 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 9b446db1b43..a6d8a19611c 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: 11.2.7 + RELEASE_VERSION: 11.2.8 DOCKER_CHAIN_NAME: celo CHAIN_TYPE: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 diff --git a/.github/workflows/publish-docker-image-for-eden.yml b/.github/workflows/publish-docker-image-for-eden.yml index b57ac97332b..ba1c3ed409e 100644 --- a/.github/workflows/publish-docker-image-for-eden.yml +++ b/.github/workflows/publish-docker-image-for-eden.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 11.2.7 + RELEASE_VERSION: 11.2.8 DOCKER_CHAIN_NAME: eden 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 ea9561ea717..85558f950e1 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 b29691400b5..6188d8cfd06 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 bb84b762e06..29797dfda88 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 1f044222c6e..514ad5f9b50 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 0b297caabad..3040fd63d46 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: 11.2.7 + RELEASE_VERSION: 11.2.8 DOCKER_CHAIN_NAME: xdai 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 77e1fe3369b..9c8a78e99e8 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 07acf64c37c..e47d6fd16c2 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 dfc276fe069..2fb971c5301 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 5e758bcc803..fa76dc51acd 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 bc4cfe7d9d0..b295199a5c3 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 097f7e3c6ca..baaa69fa14a 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: 11.2.7 + RELEASE_VERSION: 11.2.8 DOCKER_CHAIN_NAME: zilliqa 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 662d1f97949..c92d2705adc 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 ebc007c2d3b..4287847298a 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index d515aa9d6b9..972c1e5342d 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 972243dabd7..09b5f1d6064 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: 11.2.7 + RELEASE_VERSION: 11.2.8 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 8a4d55c2c86..e3ba72ae5e4 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eden.yml b/.github/workflows/release-eden.yml index dd18b2c1d5d..702fba733ca 100644 --- a/.github/workflows/release-eden.yml +++ b/.github/workflows/release-eden.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index a86060de68b..86e0d5fff43 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 8bc6ddfa803..c8e8d092144 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index cf2b09ca064..0dc36bba601 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 6a724e83c7e..e6b993fd217 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 5698ed3acdb..5950116ce3b 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 0da07d47a20..3ea8fc688b9 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index fd77fa5fc3b..e45e92b1b15 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 5e327852df4..5af2996935d 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 29804321a11..8680c3469a6 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index b97c860dca5..0d4166997ac 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: 11.2.7 + RELEASE_VERSION: 11.2.8 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d2817e91d..46c168cae40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 11.2.8 + +### 🚀 Features + +- Add missing address native coin balances count indexer metric ([#14729](https://github.com/blockscout/blockscout/issues/14729)) +- Add market source requests metric with endpoint type labels ([#14725](https://github.com/blockscout/blockscout/issues/14725)) + +### 🐛 Bug Fixes + +- Use cgroup memory limit for memory monitor in containers ([#14739](https://github.com/blockscout/blockscout/issues/14739)) + +### 🚜 Refactor + +- Switch TAC operations in search to Read API v2 ([#14719](https://github.com/blockscout/blockscout/issues/14719)) + +### ⚡ Performance + +- Batch on-demand internal transaction trace requests ([#14724](https://github.com/blockscout/blockscout/issues/14724)) + + ## 11.2.7 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 2f744302126..02eb9583afc 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -20,7 +20,7 @@ defmodule BlockScoutWeb.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "11.2.7", + version: "11.2.8", xref: [ exclude: [ Explorer.Chain.Beacon.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 969822e6ebc..9012839b1bf 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -20,7 +20,7 @@ defmodule EthereumJSONRPC.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "11.2.7" + version: "11.2.8" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 0d97ce22b91..41cc04a1811 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -15,7 +15,7 @@ defmodule Explorer.Token.MetadataRetriever do @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" @invalid_ipfs_path "invalid ipfs path" - @default_headers [{"User-Agent", "blockscout-11.2.7"}] + @default_headers [{"User-Agent", "blockscout-11.2.8"}] # 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 70b87a8fddd..616f7957fa1 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -21,7 +21,7 @@ defmodule Explorer.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "11.2.7", + version: "11.2.8", 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 cfc7f0a0659..dcebfe8e14c 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -15,7 +15,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "11.2.7", + version: "11.2.8", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 5351585bb8f..4f555517dfb 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -5,7 +5,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "11.2.7", + version: "11.2.8", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index b3604f8506e..28791ffd11f 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -5,7 +5,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "11.2.7", + version: "11.2.8", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index c481f1399ed..fb2c95989c8 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 ?= '11.2.7' +RELEASE_VERSION ?= '11.2.8' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 9221e1e8b40..40a79271c0d 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "11.2.7", + version: "11.2.8", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index f44c8d5f20e..58d2d5fe9c9 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -72,7 +72,7 @@ end # will be used by default release :blockscout do - set version: "11.2.7" + set version: "11.2.8" set applications: [ :runtime_tools, block_scout_web: :permanent, From 01a56de270dfbbdf4584262a3824ae6f8a30da82 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 26 Aug 2026 12:31:06 +0300 Subject: [PATCH 11/11] v11.2.8 new env vars in CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c168cae40..ff58a4687cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ - Batch on-demand internal transaction trace requests ([#14724](https://github.com/blockscout/blockscout/issues/14724)) +### New ENV variables + +| Variable | Description | Parameters | +|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| `INDEXER_ON_DEMAND_INTERNAL_TRANSACTIONS_BLOCKS_BATCH_SIZE` | Batch size for block trace requests in the on-demand internal transactions fetcher. The minimum value is `1`. Implemented in [#14724](https://github.com/blockscout/blockscout/pull/14724). | Version: v11.2.8\+
Default: `2`
Applications: Indexer | +| `INDEXER_ON_DEMAND_INTERNAL_TRANSACTIONS_TRANSACTIONS_BATCH_SIZE` | Batch size for transaction trace requests in the on-demand internal transactions fetcher. The minimum value is `1`. Implemented in [#14724](https://github.com/blockscout/blockscout/pull/14724). | Version: v11.2.8\+
Default: `20`
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_MISSING_ADDRESS_NATIVE_COIN_BALANCES_COUNT` | Flag to enable indexer metric: the count of address native coin balances with missing values. Implemented in [#14729](https://github.com/blockscout/blockscout/pull/14729). | Version: v11.2.8\+
Default: true
Applications: Indexer | + ## 11.2.7