diff --git a/.gitignore b/.gitignore
index 66474f1a31b..07b93167e77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ dump.rdb
.cursorrules
.elixir_ls
.claude/settings.local.json
+.claude/commands/review-pr.md
**.dec**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3d2817e91d..ff58a4687cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# 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))
+
+### 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
### 🚀 Features
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/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/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/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/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/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/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/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/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/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/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
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/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)
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/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/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/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
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/config/runtime.exs b/config/runtime.exs
index 333588105cf..2c603c36726 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -1202,7 +1202,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")
@@ -1753,7 +1757,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/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",
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index 6807bcbc9e0..d93e81c7034 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -656,6 +656,7 @@ SOURCIFY_REPO_URL=https://repo.sourcify.dev/contracts/
# MUD_POOL_SIZE=50
# WETH_TOKEN_TRANSFERS_FILTERING_ENABLED=false
# WHITELISTED_WETH_CONTRACTS=
+<<<<<<< HEAD
PUBLIC_METRICS_ENABLED=true
PUBLIC_METRICS_UPDATE_PERIOD_HOURS=1
INDEXER_METRICS_ENABLED=true
@@ -664,6 +665,17 @@ INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT=true
INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT=true
INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT=true
INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT=true
+=======
+# PUBLIC_METRICS_ENABLED=
+# PUBLIC_METRICS_UPDATE_PERIOD_HOURS=
+# INDEXER_METRICS_ENABLED=
+# INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT=
+# INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT=
+# INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT=
+# INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT=
+# INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT=
+# INDEXER_METRICS_ENABLED_MISSING_ADDRESS_NATIVE_COIN_BALANCES_COUNT=
+>>>>>>> v11.2.8
# INDEXER_REALTIME_METRICS_ENABLED=
# CSV_EXPORT_LIMIT=
# CSV_EXPORT_ASYNC_ENABLED=
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/mix.lock b/mix.lock
index d16e6370df1..70574949b4a 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,8 +136,13 @@
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"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"},
+<<<<<<< HEAD
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.7.0", "fb1e429f6d8778ce3a6962debdc5e555428a05a6e7b058d6dbad13d281a2c31f", [: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", "dc9f44271aa6fc4ab7797f2aa374ba096ef2c87520586280eb095626b7387a68"},
"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_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.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"},
+>>>>>>> v11.2.8
"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"},
@@ -172,7 +177,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"},
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,