Add stability validators (#9390)

* Add stability validators

* Process review comments

* Fix tests
pull/9537/head
nikitosing 9 months ago committed by GitHub
parent fdbb881a18
commit 546b732ac1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 9
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  3. 28
      apps/block_scout_web/lib/block_scout_web/chain.ex
  4. 83
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex
  5. 40
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  6. 17
      apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex
  7. 205
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs
  8. 1
      apps/block_scout_web/test/test_helper.exs
  9. 5
      apps/explorer/config/config.exs
  10. 2
      apps/explorer/config/dev.exs
  11. 4
      apps/explorer/config/prod.exs
  12. 4
      apps/explorer/config/test.exs
  13. 14
      apps/explorer/lib/explorer/application.ex
  14. 105
      apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex
  15. 288
      apps/explorer/lib/explorer/chain/stability/validator.ex
  16. 10
      apps/explorer/lib/explorer/repo.ex
  17. 14
      apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs
  18. 10
      apps/explorer/test/support/factory.ex
  19. 1
      apps/explorer/test/test_helper.exs
  20. 14
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  21. 70
      apps/indexer/lib/indexer/fetcher/stability/validator.ex
  22. 16
      apps/indexer/lib/indexer/supervisor.ex
  23. 270
      apps/indexer/test/indexer/fetcher/stability/validator_test.exs
  24. 1
      config/config_helper.exs
  25. 9
      config/runtime/dev.exs
  26. 8
      config/runtime/prod.exs

@ -7,6 +7,7 @@
- [#9490](https://github.com/blockscout/blockscout/pull/9490) - Add blob transaction counter and filter in block view
- [#9461](https://github.com/blockscout/blockscout/pull/9461) - Fetch blocks without internal transactions backwards
- [#9460](https://github.com/blockscout/blockscout/pull/9460) - Optimism chain type
- [#9390](https://github.com/blockscout/blockscout/pull/9390) - Add stability validators
- [#8702](https://github.com/blockscout/blockscout/pull/8702) - Add OP withdrawal status to transaction page in API
- [#7200](https://github.com/blockscout/blockscout/pull/7200) - Add Optimism BedRock Deposits to the main page in API
- [#6980](https://github.com/blockscout/blockscout/pull/6980) - Add Optimism BedRock support (Txn Batches, Output Roots, Deposits, Withdrawals)

@ -369,6 +369,15 @@ defmodule BlockScoutWeb.ApiRouter do
get("/:blob_hash_param", V2.BlobController, :blob)
end
end
scope "/validators" do
if Application.compile_env(:explorer, :chain_type) == "stability" do
scope "/stability" do
get("/", V2.ValidatorController, :stability_validators_list)
get("/counters", V2.ValidatorController, :stability_validators_counters)
end
end
end
end
scope "/v1", as: :api_v1 do

@ -17,7 +17,9 @@ defmodule BlockScoutWeb.Chain do
import Explorer.Helper, only: [parse_integer: 1]
alias BlockScoutWeb.PagingHelper
alias Ecto.Association.NotLoaded
alias Explorer.Chain.UserOperation
alias Explorer.Account.{TagAddress, TagTransaction, WatchlistAddress}
alias Explorer.Chain.Beacon.Reader, as: BeaconReader
alias Explorer.Chain.Block.Reward
@ -459,6 +461,25 @@ defmodule BlockScoutWeb.Chain do
[paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_type}}]
end
# Clause for `Explorer.Chain.Stability.Validator`,
# returned by `BlockScoutWeb.API.V2.ValidatorController.stability_validators_list/2` (`/api/v2/validators/stability`)
def paging_options(%{
"state" => state,
"address_hash" => address_hash_string,
"blocks_validated" => blocks_validated_string
}) do
[
paging_options: %{
@default_paging_options
| key: %{
address_hash: parse_address_hash(address_hash_string),
blocks_validated: parse_integer(blocks_validated_string),
state: if(state in PagingHelper.allowed_stability_validators_states(), do: state)
}
}
]
end
def paging_options(_params), do: [paging_options: @default_paging_options]
def put_key_value_to_paging_options([paging_options: paging_options], key, value) do
@ -541,6 +562,13 @@ defmodule BlockScoutWeb.Chain do
end
end
defp parse_address_hash(address_hash_string) do
case Hash.Address.cast(address_hash_string) do
{:ok, address_hash} -> address_hash
_ -> nil
end
end
defp paging_params({%Address{hash: hash, fetched_coin_balance: fetched_coin_balance}, _}) do
%{"hash" => hash, "fetched_coin_balance" => Decimal.to_string(fetched_coin_balance.value)}
end

@ -0,0 +1,83 @@
defmodule BlockScoutWeb.API.V2.ValidatorController do
use BlockScoutWeb, :controller
alias Explorer.Chain.Cache.StabilityValidatorsCounters
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
import BlockScoutWeb.PagingHelper,
only: [
delete_parameters_from_next_page_params: 1,
stability_validators_state_options: 1,
validators_stability_sorting: 1
]
import BlockScoutWeb.Chain,
only: [
split_list_by_page: 1,
paging_options: 1,
next_page_params: 4
]
@api_true api?: true
@doc """
Function to handle GET requests to `/api/v2/validators/stability` endpoint.
"""
@spec stability_validators_list(Plug.Conn.t(), map()) :: Plug.Conn.t()
def stability_validators_list(conn, params) do
options =
[
necessity_by_association: %{
:address => :optional
}
]
|> Keyword.merge(@api_true)
|> Keyword.merge(paging_options(params))
|> Keyword.merge(validators_stability_sorting(params))
|> Keyword.merge(stability_validators_state_options(params))
{validators, next_page} = options |> ValidatorStability.get_paginated_validators() |> split_list_by_page()
next_page_params =
next_page
|> next_page_params(
validators,
delete_parameters_from_next_page_params(params),
&ValidatorStability.next_page_params/1
)
conn
|> render(:stability_validators, %{validators: validators, next_page_params: next_page_params})
end
@doc """
Function to handle GET requests to `/api/v2/validators/stability/counters` endpoint.
"""
@spec stability_validators_counters(Plug.Conn.t(), map()) :: Plug.Conn.t()
def stability_validators_counters(conn, _params) do
%{
validators_counter: validators_counter,
new_validators_counter: new_validators_counter,
active_validators_counter: active_validators_counter
} = StabilityValidatorsCounters.get_counters(@api_true)
conn
|> json(%{
validators_counter: validators_counter,
new_validators_counter_24h: new_validators_counter,
active_validators_counter: active_validators_counter,
active_validators_percentage:
calculate_active_validators_percentage(active_validators_counter, validators_counter)
})
end
defp calculate_active_validators_percentage(active_validators_counter, validators_counter) do
if Decimal.compare(validators_counter, Decimal.new(0)) == :gt do
active_validators_counter
|> Decimal.div(validators_counter)
|> Decimal.mult(100)
|> Decimal.to_float()
|> Float.floor(2)
end
end
end

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.PagingHelper do
Helper for fetching filters and other url query parameters
"""
import Explorer.Chain, only: [string_to_transaction_hash: 1]
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
alias Explorer.Chain.Transaction
alias Explorer.{Helper, PagingOptions, SortingHelper}
@ -34,6 +35,9 @@ defmodule BlockScoutWeb.PagingHelper do
@allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"]
@allowed_nft_token_type_labels ["ERC-721", "ERC-1155"]
@allowed_chain_id [1, 56, 99]
@allowed_stability_validators_states ["active", "probation", "inactive"]
def allowed_stability_validators_states, do: @allowed_stability_validators_states
def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do
with {block_number, ""} <- Integer.parse(block_number_string),
@ -57,6 +61,13 @@ defmodule BlockScoutWeb.PagingHelper do
def paging_options(_params, _filter), do: [paging_options: @default_paging_options]
@spec stability_validators_state_options(map()) :: [{:state, list()}, ...]
def stability_validators_state_options(%{"state_filter" => state}) do
[state: filters_to_list(state, @allowed_stability_validators_states, :downcase)]
end
def stability_validators_state_options(_), do: [state: []]
@spec token_transfers_types_options(map()) :: [{:token_type, list}]
def token_transfers_types_options(%{"type" => filters}) do
[
@ -78,7 +89,9 @@ defmodule BlockScoutWeb.PagingHelper do
def nft_token_types_options(_), do: [token_type: []]
defp filters_to_list(filters, allowed), do: filters |> String.upcase() |> parse_filter(allowed)
defp filters_to_list(filters, allowed, variant \\ :upcase)
defp filters_to_list(filters, allowed, :downcase), do: filters |> String.downcase() |> parse_filter(allowed)
defp filters_to_list(filters, allowed, :upcase), do: filters |> String.upcase() |> parse_filter(allowed)
# sobelow_skip ["DOS.StringToAtom"]
def filter_options(%{"filter" => filter}, fallback) do
@ -188,7 +201,8 @@ defmodule BlockScoutWeb.PagingHelper do
"filter",
"q",
"sort",
"order"
"order",
"state_filter"
])
end
@ -267,4 +281,26 @@ defmodule BlockScoutWeb.PagingHelper do
do: [{:dynamic, :fee, :desc_nulls_last, Transaction.dynamic_fee()}]
defp do_address_transaction_sorting(_, _), do: []
@spec validators_stability_sorting(%{required(String.t()) => String.t()}) :: [
{:sorting, SortingHelper.sorting_params()}
]
def validators_stability_sorting(%{"sort" => sort_field, "order" => order}) do
[sorting: do_validators_stability_sorting(sort_field, order)]
end
def validators_stability_sorting(_), do: []
defp do_validators_stability_sorting("state", "asc"), do: [asc_nulls_first: :state]
defp do_validators_stability_sorting("state", "desc"), do: [desc_nulls_last: :state]
defp do_validators_stability_sorting("address_hash", "asc"), do: [asc_nulls_first: :address_hash]
defp do_validators_stability_sorting("address_hash", "desc"), do: [desc_nulls_last: :address_hash]
defp do_validators_stability_sorting("blocks_validated", "asc"),
do: [{:dynamic, :blocks_validated, :asc_nulls_first, ValidatorStability.dynamic_validated_blocks()}]
defp do_validators_stability_sorting("blocks_validated", "desc"),
do: [{:dynamic, :blocks_validated, :desc_nulls_last, ValidatorStability.dynamic_validated_blocks()}]
defp do_validators_stability_sorting(_, _), do: []
end

@ -0,0 +1,17 @@
defmodule BlockScoutWeb.API.V2.ValidatorView do
use BlockScoutWeb, :view
alias BlockScoutWeb.API.V2.Helper
def render("stability_validators.json", %{validators: validators, next_page_params: next_page_params}) do
%{"items" => Enum.map(validators, &prepare_validator(&1)), "next_page_params" => next_page_params}
end
defp prepare_validator(validator) do
%{
"address" => Helper.address_with_info(nil, validator.address, validator.address_hash, true),
"state" => validator.state,
"blocks_validated_count" => validator.blocks_validated
}
end
end

@ -0,0 +1,205 @@
defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.Address
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
alias Explorer.Chain.Cache.StabilityValidatorsCounters
if Application.compile_env(:explorer, :chain_type) == "stability" do
describe "/validators/stability" do
test "get paginated list of the validators", %{conn: conn} do
validators =
insert_list(51, :validator_stability)
|> Enum.sort_by(
fn validator ->
{Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes}
end,
:desc
)
request = get(conn, "/api/v2/validators/stability")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/validators/stability", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, validators)
end
test "sort by blocks_validated asc", %{conn: conn} do
validators =
for _ <- 0..50 do
validator = insert(:validator_stability)
blocks_count = Enum.random(0..50)
_ =
for _ <- 0..blocks_count do
insert(:block, miner_hash: validator.address_hash, miner: nil)
end
{validator, blocks_count}
end
|> Enum.sort(&compare_default_sorting_for_asc/2)
init_params = %{"sort" => "blocks_validated", "order" => "asc"}
request = get(conn, "/api/v2/validators/stability", init_params)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"]))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, validators)
end
test "sort by blocks_validated desc", %{conn: conn} do
validators =
for _ <- 0..50 do
validator = insert(:validator_stability)
blocks_count = Enum.random(0..50)
_ =
for _ <- 0..blocks_count do
insert(:block, miner_hash: validator.address_hash, miner: nil)
end
{validator, blocks_count}
end
|> Enum.sort(&compare_default_sorting_for_desc/2)
init_params = %{"sort" => "blocks_validated", "order" => "desc"}
request = get(conn, "/api/v2/validators/stability", init_params)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"]))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, validators)
end
test "state_filter=probation", %{conn: conn} do
insert_list(51, :validator_stability, state: Enum.random([:active, :inactive]))
validators =
insert_list(51, :validator_stability, state: :probation)
|> Enum.sort_by(
fn validator ->
{Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes}
end,
:desc
)
init_params = %{"state_filter" => "probation"}
request = get(conn, "/api/v2/validators/stability", init_params)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"]))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, validators)
end
end
describe "/validators/stability/counters" do
test "get counters", %{conn: conn} do
_validator_active1 =
insert(:validator_stability, state: :active, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day))
_validator_active2 = insert(:validator_stability, state: :active)
_validator_active3 = insert(:validator_stability, state: :active)
_validator_inactive1 =
insert(:validator_stability, state: :inactive, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day))
_validator_inactive2 = insert(:validator_stability, state: :inactive)
_validator_inactive3 = insert(:validator_stability, state: :inactive)
_validator_probation1 =
insert(:validator_stability, state: :probation, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day))
_validator_probation2 = insert(:validator_stability, state: :probation)
_validator_probation3 = insert(:validator_stability, state: :probation)
StabilityValidatorsCounters.consolidate()
:timer.sleep(500)
percentage = (3 / 9 * 100) |> Float.floor(2)
request = get(conn, "/api/v2/validators/stability/counters")
assert %{
"active_validators_counter" => "3",
"active_validators_percentage" => ^percentage,
"new_validators_counter_24h" => "6",
"validators_counter" => "9"
} = json_response(request, 200)
end
end
end
defp compare_item(%ValidatorStability{} = validator, json) do
assert Address.checksum(validator.address_hash) == json["address"]["hash"]
assert to_string(validator.state) == json["state"]
end
defp compare_item({%ValidatorStability{} = validator, count}, json) do
assert json["blocks_validated_count"] == count + 1
assert compare_item(validator, json)
end
defp check_paginated_response(first_page_resp, second_page_resp, list) do
assert Enum.count(first_page_resp["items"]) == 50
assert first_page_resp["next_page_params"] != nil
compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
assert Enum.count(second_page_resp["items"]) == 1
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
end
defp compare_default_sorting_for_asc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do
case {
compare(blocks_count_1, blocks_count_2),
compare(
Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state),
Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state)
),
compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes)
} do
{:lt, _, _} -> false
{:eq, :lt, _} -> false
{:eq, :eq, :lt} -> false
_ -> true
end
end
defp compare_default_sorting_for_desc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do
case {
compare(blocks_count_1, blocks_count_2),
compare(
Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state),
Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state)
),
compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes)
} do
{:gt, _, _} -> false
{:eq, :lt, _} -> false
{:eq, :eq, :lt} -> false
_ -> true
end
end
defp compare(a, b) do
cond do
a < b -> :lt
a > b -> :gt
true -> :eq
end
end
end

@ -32,6 +32,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :manual)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :manual)

@ -76,6 +76,11 @@ config :explorer, Explorer.Chain.Cache.WithdrawalsSum,
enable_consolidation: true,
update_interval_in_milliseconds: update_interval_in_milliseconds_default
config :explorer, Explorer.Chain.Cache.StabilityValidatorsCounters,
enabled: true,
enable_consolidation: true,
update_interval_in_milliseconds: update_interval_in_milliseconds_default
config :explorer, Explorer.Chain.Cache.TransactionActionTokensData, enabled: true
config :explorer, Explorer.Chain.Cache.TransactionActionUniswapPools, enabled: true

@ -32,6 +32,8 @@ config :explorer, Explorer.Repo.BridgedTokens, timeout: :timer.seconds(80)
config :explorer, Explorer.Repo.Filecoin, timeout: :timer.seconds(80)
config :explorer, Explorer.Repo.Stability, timeout: :timer.seconds(80)
config :explorer, Explorer.Tracer, env: "dev", disabled?: true
config :logger, :explorer,

@ -52,6 +52,10 @@ config :explorer, Explorer.Repo.Filecoin,
prepare: :unnamed,
timeout: :timer.seconds(60)
config :explorer, Explorer.Repo.Stability,
prepare: :unnamed,
timeout: :timer.seconds(60)
config :explorer, Explorer.Tracer, env: "production", disabled?: true
config :logger, :explorer,

@ -52,7 +52,8 @@ for repo <- [
Explorer.Repo.Shibarium,
Explorer.Repo.Suave,
Explorer.Repo.BridgedTokens,
Explorer.Repo.Filecoin
Explorer.Repo.Filecoin,
Explorer.Repo.Stability
] do
config :explorer, repo,
database: "explorer_test",
@ -84,3 +85,4 @@ config :explorer, Explorer.ExchangeRates.Source.TransactionAndLog,
config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false
config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: false
config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: false

@ -132,7 +132,8 @@ defmodule Explorer.Application do
configure(Explorer.Migrator.AddressTokenBalanceTokenType),
configure(Explorer.Migrator.SanitizeMissingBlockRanges),
configure(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers),
configure(Explorer.Migrator.TokenTransferTokenType)
configure(Explorer.Migrator.TokenTransferTokenType),
configure_chain_type_dependent_process(Explorer.Chain.Cache.StabilityValidatorsCounters, "stability")
]
|> List.flatten()
@ -150,7 +151,8 @@ defmodule Explorer.Application do
Explorer.Repo.Shibarium,
Explorer.Repo.Suave,
Explorer.Repo.BridgedTokens,
Explorer.Repo.Filecoin
Explorer.Repo.Filecoin,
Explorer.Repo.Stability
]
else
[]
@ -177,6 +179,14 @@ defmodule Explorer.Application do
end
end
defp configure_chain_type_dependent_process(process, chain_type) do
if Application.get_env(:explorer, :chain_type) == chain_type do
process
else
[]
end
end
defp sc_microservice_configure(process) do
if Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:eth_bytecode_db?] do
process

@ -0,0 +1,105 @@
defmodule Explorer.Chain.Cache.StabilityValidatorsCounters do
@moduledoc """
Counts and store counters of validators stability.
It loads the count asynchronously and in a time interval of 30 minutes.
"""
use GenServer
alias Explorer.Chain
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
@validators_counter_key "stability_validators_counter"
@new_validators_counter_key "new_stability_validators_counter"
@active_validators_counter_key "active_stability_validators_counter"
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.compile_env(:explorer, __MODULE__)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@update_interval_in_milliseconds Keyword.get(config, :update_interval_in_milliseconds)
@doc """
Starts a process to periodically update validators stability counters
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(_args) do
{:ok, %{consolidate?: @enable_consolidation}, {:continue, :ok}}
end
defp schedule_next_consolidation do
Process.send_after(self(), :consolidate, @update_interval_in_milliseconds)
end
@impl true
def handle_continue(:ok, %{consolidate?: true} = state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@impl true
def handle_continue(:ok, state) do
{:noreply, state}
end
@impl true
def handle_info(:consolidate, state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@doc """
Fetches values for a stability validators counters from the `last_fetched_counters` table.
"""
@spec get_counters(Keyword.t()) :: map()
def get_counters(options) do
%{
validators_counter: Chain.get_last_fetched_counter(@validators_counter_key, options),
new_validators_counter: Chain.get_last_fetched_counter(@new_validators_counter_key, options),
active_validators_counter: Chain.get_last_fetched_counter(@active_validators_counter_key, options)
}
end
@doc """
Consolidates the info by populating the `last_fetched_counters` table with the current database information.
"""
@spec consolidate() :: any()
def consolidate do
tasks = [
Task.async(fn -> ValidatorStability.count_validators() end),
Task.async(fn -> ValidatorStability.count_new_validators() end),
Task.async(fn -> ValidatorStability.count_active_validators() end)
]
[validators_counter, new_validators_counter, active_validators_counter] = Task.await_many(tasks, :infinity)
Chain.upsert_last_fetched_counter(%{
counter_type: @validators_counter_key,
value: validators_counter
})
Chain.upsert_last_fetched_counter(%{
counter_type: @new_validators_counter_key,
value: new_validators_counter
})
Chain.upsert_last_fetched_counter(%{
counter_type: @active_validators_counter_key,
value: active_validators_counter
})
end
end

@ -0,0 +1,288 @@
defmodule Explorer.Chain.Stability.Validator do
@moduledoc """
Stability validators
"""
use Explorer.Schema
alias Explorer.Chain.{Address, Import}
alias Explorer.Chain.Hash.Address, as: HashAddress
alias Explorer.{Chain, Repo, SortingHelper}
alias Explorer.SmartContract.Reader
require Logger
@default_sorting [
asc: :state,
asc: :address_hash
]
@state_enum [active: 0, probation: 1, inactive: 2]
@primary_key false
typed_schema "validators_stability" do
field(:address_hash, HashAddress, primary_key: true)
field(:state, Ecto.Enum, values: @state_enum)
field(:blocks_validated, :integer, virtual: true)
has_one(:address, Address, foreign_key: :hash, references: :address_hash)
timestamps()
end
@required_attrs ~w(address_hash)a
@optional_attrs ~w(state)a
def changeset(%__MODULE__{} = validator, attrs) do
validator
|> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:address_hash)
end
@doc """
Get validators list.
Keyword could contain:
- paging_options
- necessity_by_association
- sorting (supported by `Explorer.SortingHelper` module)
- state (one of `@state_enum`)
"""
@spec get_paginated_validators(keyword()) :: [t()]
def get_paginated_validators(options \\ []) do
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options())
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
sorting = Keyword.get(options, :sorting, [])
states = Keyword.get(options, :state, [])
__MODULE__
|> apply_filter_by_state(states)
|> select_merge([vs], %{
blocks_validated:
fragment(
"SELECT count(*) FROM blocks WHERE miner_hash = ?",
vs.address_hash
)
})
|> Chain.join_associations(necessity_by_association)
|> SortingHelper.apply_sorting(sorting, @default_sorting)
|> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting)
|> Chain.select_repo(options).all()
end
defp apply_filter_by_state(query, []), do: query
defp apply_filter_by_state(query, states) do
query
|> where([vs], vs.state in ^states)
end
@doc """
Get all validators
"""
@spec get_all_validators(keyword()) :: [t()]
def get_all_validators(options \\ []) do
__MODULE__
|> Chain.select_repo(options).all()
end
@get_active_validator_list_abi %{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}],
"name" => "getActiveValidatorList",
"inputs" => []
}
@get_validator_list_abi %{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}],
"name" => "getValidatorList",
"inputs" => []
}
@get_validator_missing_blocks_abi %{
"inputs" => [
%{
"internalType" => "address",
"name" => "validator",
"type" => "address"
}
],
"name" => "getValidatorMissingBlocks",
"outputs" => [
%{
"internalType" => "uint256",
"name" => "",
"type" => "uint256"
}
],
"stateMutability" => "view",
"type" => "function"
}
@get_active_validator_list_method_id "a5aa7380"
@get_validator_list_method_id "e35c0f7d"
@get_validator_missing_blocks_method_id "41ee9a53"
@stability_validator_controller_contract "0x0000000000000000000000000000000000000805"
@doc """
Do batch eth_call of `getValidatorList` and `getActiveValidatorList` methods to `@stability_validator_controller_contract`.
Returns a map with two lists: `active` and `all`, or nil if error.
"""
@spec fetch_validators_lists :: nil | %{active: list(binary()), all: list(binary())}
def fetch_validators_lists do
abi = [@get_active_validator_list_abi, @get_validator_list_abi]
params = %{@get_validator_list_method_id => [], @get_active_validator_list_method_id => []}
case Reader.query_contract(@stability_validator_controller_contract, abi, params, false) do
%{
@get_active_validator_list_method_id => {:ok, [active_validators_list]},
@get_validator_list_method_id => {:ok, [validators_list]}
} ->
%{active: active_validators_list, all: validators_list}
error ->
Logger.warn(fn -> ["Error on getting validator lists: #{inspect(error)}"] end)
nil
end
end
@doc """
Do batch eth_call of `getValidatorMissingBlocks` method to #{@stability_validator_controller_contract}.
Accept: list of validator address hashes
Returns a map: validator_address_hash => missing_blocks_number
"""
@spec fetch_missing_blocks_numbers(list(binary())) :: map()
def fetch_missing_blocks_numbers(validators_address_hashes) do
validators_address_hashes
|> Enum.map(&format_request_missing_blocks_number/1)
|> Reader.query_contracts([@get_validator_missing_blocks_abi])
|> Enum.zip_reduce(validators_address_hashes, %{}, fn response, address_hash, acc ->
result =
case format_missing_blocks_result(response) do
{:error, message} ->
Logger.warn(fn -> ["Error on getValidatorMissingBlocks for #{validators_address_hashes}: #{message}"] end)
nil
amount ->
amount
end
Map.put(acc, address_hash, result)
end)
end
defp format_missing_blocks_result({:ok, [amount]}) do
amount
end
defp format_missing_blocks_result({:error, error_message}) do
{:error, error_message}
end
defp format_request_missing_blocks_number(address_hash) do
%{
contract_address: @stability_validator_controller_contract,
method_id: @get_validator_missing_blocks_method_id,
args: [address_hash]
}
end
@doc """
Convert missing block number to state
"""
@spec missing_block_number_to_state(integer()) :: atom()
def missing_block_number_to_state(integer) when integer > 0, do: :probation
def missing_block_number_to_state(integer) when integer == 0, do: :active
def missing_block_number_to_state(_), do: nil
@doc """
Delete validators by address hashes
"""
@spec delete_validators_by_address_hashes([binary() | HashAddress.t()]) :: {non_neg_integer(), nil | []} | :ignore
def delete_validators_by_address_hashes(list) when is_list(list) and length(list) > 0 do
__MODULE__
|> where([vs], vs.address_hash in ^list)
|> Repo.delete_all()
end
def delete_validators_by_address_hashes(_), do: :ignore
@doc """
Insert validators
"""
@spec insert_validators([map()]) :: {non_neg_integer(), nil | []}
def insert_validators(validators) do
Repo.insert_all(__MODULE__, validators,
on_conflict: {:replace_all_except, [:inserted_at]},
conflict_target: [:address_hash]
)
end
@doc """
Append timestamps (:inserted_at, :updated_at)
"""
@spec append_timestamps(map()) :: map()
def append_timestamps(validator) do
Map.merge(validator, Import.timestamps())
end
@doc """
Derive next page params from %Explorer.Chain.Stability.Validator{}
"""
@spec next_page_params(t()) :: map()
def next_page_params(%__MODULE__{state: state, address_hash: address_hash, blocks_validated: blocks_validated}) do
%{"state" => state, "address_hash" => address_hash, "blocks_validated" => blocks_validated}
end
@doc """
Returns state enum
"""
@spec state_enum() :: Keyword.t()
def state_enum, do: @state_enum
@doc """
Returns dynamic query for validated blocks count. Needed for SortingHelper
"""
@spec dynamic_validated_blocks() :: Ecto.Query.dynamic_expr()
def dynamic_validated_blocks do
dynamic(
[vs],
fragment(
"SELECT count(*) FROM blocks WHERE miner_hash = ?",
vs.address_hash
)
)
end
@doc """
Returns total count of validators.
"""
@spec count_validators() :: integer()
def count_validators do
Repo.aggregate(__MODULE__, :count, :address_hash)
end
@doc """
Returns count of new validators (inserted withing last 24h).
"""
@spec count_new_validators() :: integer()
def count_new_validators do
__MODULE__
|> where([vs], vs.inserted_at >= ago(1, "day"))
|> Repo.aggregate(:count, :address_hash)
end
@doc """
Returns count of active validators.
"""
@spec count_active_validators() :: integer()
def count_active_validators do
__MODULE__
|> where([vs], vs.state == :active)
|> Repo.aggregate(:count, :address_hash)
end
end

@ -240,4 +240,14 @@ defmodule Explorer.Repo do
ConfigHelper.init_repo_module(__MODULE__, opts)
end
end
defmodule Stability do
use Ecto.Repo,
otp_app: :explorer,
adapter: Ecto.Adapters.Postgres
def init(_, opts) do
ConfigHelper.init_repo_module(__MODULE__, opts)
end
end
end

@ -0,0 +1,14 @@
defmodule Explorer.Repo.Stability.Migrations.AddStabilityValidators do
use Ecto.Migration
def change do
create table(:validators_stability, primary_key: false) do
add(:address_hash, :bytea, null: false, primary_key: true)
add(:state, :integer, default: 0)
timestamps()
end
create_if_not_exists(index(:validators_stability, ["state ASC", "address_hash ASC"]))
end
end

@ -23,6 +23,7 @@ defmodule Explorer.Factory do
alias Explorer.Admin.Administrator
alias Explorer.Chain.Beacon.{Blob, BlobTransaction}
alias Explorer.Chain.Block.{EmissionReward, Range, Reward}
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
alias Explorer.Chain.{
Address,
@ -1116,4 +1117,13 @@ defmodule Explorer.Factory do
end
def random_bool, do: Enum.random([true, false])
def validator_stability_factory do
address = insert(:address)
%ValidatorStability{
address_hash: address.hash,
state: Enum.random(0..2)
}
end
end

@ -21,6 +21,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :auto)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :auto)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :auto)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :auto)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :auto)
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Mox.defmock(Explorer.Market.History.Source.Price.TestSource, for: Explorer.Market.History.Source.Price)

@ -130,6 +130,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
new_previous_number =
case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do
{:ok, number} when is_nil(previous_number) or number != previous_number ->
number =
if abnormal_gap?(number, previous_number) do
new_number = max(number, previous_number)
start_fetch_and_import(new_number, block_fetcher, previous_number)
@ -139,6 +140,9 @@ defmodule Indexer.Block.Realtime.Fetcher do
number
end
fetch_validators_async()
number
_ ->
previous_number
end
@ -158,6 +162,16 @@ defmodule Indexer.Block.Realtime.Fetcher do
{:noreply, state}
end
if Application.compile_env(:explorer, :chain_type) == "stability" do
defp fetch_validators_async do
GenServer.cast(Indexer.Fetcher.Stability.Validator, :update_validators_list)
end
else
defp fetch_validators_async do
:ignore
end
end
defp subscribe_to_new_heads(%__MODULE__{subscription: nil} = state, subscribe_named_arguments)
when is_list(subscribe_named_arguments) do
case EthereumJSONRPC.subscribe("newHeads", subscribe_named_arguments) do

@ -0,0 +1,70 @@
defmodule Indexer.Fetcher.Stability.Validator do
@moduledoc """
GenServer responsible for updating the list of stability validators in the database.
"""
use GenServer
alias Explorer.Chain.Hash.Address, as: AddressHash
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(state) do
GenServer.cast(__MODULE__, :update_validators_list)
{:ok, state}
end
def handle_cast(:update_validators_list, state) do
validators_from_db = ValidatorStability.get_all_validators()
case ValidatorStability.fetch_validators_lists() do
%{active: active_validator_addresses_list, all: validator_addresses_list} ->
validators_map = Enum.reduce(validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end)
active_validators_map =
Enum.reduce(active_validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end)
address_hashes_to_drop_from_db =
Enum.flat_map(validators_from_db, fn validator ->
(is_nil(validators_map[validator.address_hash.bytes]) && [validator.address_hash]) || []
end)
grouped =
Enum.group_by(validator_addresses_list, fn validator_address -> active_validators_map[validator_address] end)
inactive =
Enum.map(grouped[nil] || [], fn address_hash ->
{:ok, address_hash} = AddressHash.load(address_hash)
%{address_hash: address_hash, state: :inactive} |> ValidatorStability.append_timestamps()
end)
validators_to_missing_blocks_numbers = ValidatorStability.fetch_missing_blocks_numbers(grouped[true] || [])
active =
Enum.map(grouped[true] || [], fn address_hash_init ->
{:ok, address_hash} = AddressHash.load(address_hash_init)
%{
address_hash: address_hash,
state:
ValidatorStability.missing_block_number_to_state(
validators_to_missing_blocks_numbers[address_hash_init]
)
}
|> ValidatorStability.append_timestamps()
end)
ValidatorStability.insert_validators(active ++ inactive)
ValidatorStability.delete_validators_by_address_hashes(address_hashes_to_drop_from_db)
_ ->
nil
end
{:noreply, state}
end
end

@ -20,6 +20,7 @@ defmodule Indexer.Supervisor do
alias Indexer.Block.Realtime, as: BlockRealtime
alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup
alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime
alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability
alias Indexer.Fetcher.TokenInstance.LegacySanitize, as: TokenInstanceLegacySanitize
alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime
alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry
@ -203,7 +204,10 @@ defmodule Indexer.Supervisor do
]
|> List.flatten()
all_fetchers = maybe_add_bridged_tokens_fetchers(basic_fetchers)
all_fetchers =
basic_fetchers
|> maybe_add_bridged_tokens_fetchers()
|> add_chain_type_dependent_fetchers()
Supervisor.init(
all_fetchers,
@ -228,6 +232,16 @@ defmodule Indexer.Supervisor do
end
end
defp add_chain_type_dependent_fetchers(fetchers) do
case Application.get_env(:explorer, :chain_type) do
"stability" ->
[{ValidatorStability, []} | fetchers]
_ ->
fetchers
end
end
defp configure(process, opts) do
if Application.get_env(:indexer, process)[:enabled] do
[{process, opts}]

@ -0,0 +1,270 @@
defmodule Indexer.Fetcher.Stability.ValidatorTest do
use EthereumJSONRPC.Case
use Explorer.DataCase
import Mox
alias Explorer.Chain.Stability.Validator, as: ValidatorStability
alias EthereumJSONRPC.Encoder
setup :verify_on_exit!
setup :set_mox_global
@accepts_list_of_addresses %{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}],
"name" => "getActiveValidatorList",
"inputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}]
}
@accepts_integer %{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{
"internalType" => "uint256",
"name" => "",
"type" => "uint256"
}
],
"name" => "getActiveValidatorList",
"inputs" => [
%{
"internalType" => "uint256",
"name" => "",
"type" => "uint256"
}
]
}
if Application.compile_env(:explorer, :chain_type) == "stability" do
describe "check update_validators_list" do
test "deletes absent validators" do
_validator = insert(:validator_stability)
_validator_active = insert(:validator_stability, state: :active)
_validator_inactive = insert(:validator_stability, state: :inactive)
_validator_probation = insert(:validator_stability, state: :probation)
start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator})
EthereumJSONRPC.Mox
|> expect(:json_rpc, 1, fn
[
%{
id: id_1,
jsonrpc: "2.0",
method: "eth_call",
params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"]
},
%{
id: id_2,
jsonrpc: "2.0",
method: "eth_call",
params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"]
}
],
_ ->
<<"0x", _method_id::binary-size(8), result::binary>> =
[@accepts_list_of_addresses]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([[]])
{:ok,
[
%{
id: id_1,
jsonrpc: "2.0",
result: "0x" <> result
},
%{
id: id_2,
jsonrpc: "2.0",
result: "0x" <> result
}
]}
end)
:timer.sleep(100)
assert ValidatorStability.get_all_validators() == []
end
test "updates validators" do
validator_active1 = insert(:validator_stability, state: :active)
validator_active2 = insert(:validator_stability, state: :active)
_validator_active3 = insert(:validator_stability, state: :active)
validator_inactive1 = insert(:validator_stability, state: :inactive)
validator_inactive2 = insert(:validator_stability, state: :inactive)
_validator_inactive3 = insert(:validator_stability, state: :inactive)
validator_probation1 = insert(:validator_stability, state: :probation)
validator_probation2 = insert(:validator_stability, state: :probation)
_validator_probation3 = insert(:validator_stability, state: :probation)
start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator})
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn
[
%{
id: id_1,
jsonrpc: "2.0",
method: "eth_call",
params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"]
},
%{
id: id_2,
jsonrpc: "2.0",
method: "eth_call",
params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"]
}
],
_ ->
<<"0x", _method_id::binary-size(8), result_all::binary>> =
[@accepts_list_of_addresses]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([
[
validator_active1.address_hash.bytes,
validator_active2.address_hash.bytes,
validator_inactive1.address_hash.bytes,
validator_inactive2.address_hash.bytes,
validator_probation1.address_hash.bytes,
validator_probation2.address_hash.bytes
]
])
<<"0x", _method_id::binary-size(8), result_active::binary>> =
[@accepts_list_of_addresses]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([
[
validator_active1.address_hash.bytes,
validator_inactive1.address_hash.bytes,
validator_probation1.address_hash.bytes
]
])
{:ok,
[
%{
id: id_1,
jsonrpc: "2.0",
result: "0x" <> result_active
},
%{
id: id_2,
jsonrpc: "2.0",
result: "0x" <> result_all
}
]}
end)
"0x" <> address_1 = to_string(validator_active1.address_hash)
"0x" <> address_2 = to_string(validator_inactive1.address_hash)
"0x" <> address_3 = to_string(validator_probation1.address_hash)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn
[
%{
id: id_1,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data: "0x41ee9a53000000000000000000000000" <> ^address_1,
to: "0x0000000000000000000000000000000000000805"
},
"latest"
]
},
%{
id: id_2,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data: "0x41ee9a53000000000000000000000000" <> ^address_2,
to: "0x0000000000000000000000000000000000000805"
},
"latest"
]
},
%{
id: id_3,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data: "0x41ee9a53000000000000000000000000" <> ^address_3,
to: "0x0000000000000000000000000000000000000805"
},
"latest"
]
}
],
_ ->
<<"0x", _method_id::binary-size(8), result_1::binary>> =
[@accepts_integer]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([10])
<<"0x", _method_id::binary-size(8), result_2::binary>> =
[@accepts_integer]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([1])
<<"0x", _method_id::binary-size(8), result_3::binary>> =
[@accepts_integer]
|> ABI.parse_specification()
|> Enum.at(0)
|> Encoder.encode_function_call([0])
{:ok,
[
%{
id: id_1,
jsonrpc: "2.0",
result: "0x" <> result_1
},
%{
id: id_2,
jsonrpc: "2.0",
result: "0x" <> result_2
},
%{
id: id_3,
jsonrpc: "2.0",
result: "0x" <> result_3
}
]}
end)
:timer.sleep(100)
validators = ValidatorStability.get_all_validators()
assert Enum.count(validators) == 6
map =
Enum.reduce(validators, %{}, fn validator, map -> Map.put(map, validator.address_hash.bytes, validator) end)
assert %ValidatorStability{state: :inactive} = map[validator_active2.address_hash.bytes]
assert %ValidatorStability{state: :inactive} = map[validator_inactive2.address_hash.bytes]
assert %ValidatorStability{state: :inactive} = map[validator_probation2.address_hash.bytes]
assert %ValidatorStability{state: :probation} = map[validator_active1.address_hash.bytes]
assert %ValidatorStability{state: :probation} = map[validator_inactive1.address_hash.bytes]
assert %ValidatorStability{state: :active} = map[validator_probation1.address_hash.bytes]
end
end
end
end

@ -17,6 +17,7 @@ defmodule ConfigHelper do
"shibarium" -> base_repos ++ [Explorer.Repo.Shibarium]
"suave" -> base_repos ++ [Explorer.Repo.Suave]
"filecoin" -> base_repos ++ [Explorer.Repo.Filecoin]
"stability" -> base_repos ++ [Explorer.Repo.Stability]
_ -> base_repos
end

@ -147,6 +147,15 @@ config :explorer, Explorer.Repo.Filecoin,
url: System.get_env("DATABASE_URL"),
pool_size: 1
# Configures Stability database
config :explorer, Explorer.Repo.Stability,
database: database,
hostname: hostname,
url: System.get_env("DATABASE_URL"),
# actually this repo is not started, and its pool size remains unused.
# separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type
pool_size: 1
variant = Variant.get()
Code.require_file("#{variant}.exs", "apps/explorer/config/dev")

@ -113,6 +113,14 @@ config :explorer, Explorer.Repo.Filecoin,
pool_size: 1,
ssl: ExplorerConfigHelper.ssl_enabled?()
# Configures Stability database
config :explorer, Explorer.Repo.Stability,
url: System.get_env("DATABASE_URL"),
# actually this repo is not started, and its pool size remains unused.
# separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type
pool_size: 1,
ssl: ExplorerConfigHelper.ssl_enabled?()
variant = Variant.get()
Code.require_file("#{variant}.exs", "apps/explorer/config/prod")

Loading…
Cancel
Save