<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
index 75d5c126d8..bf41494b38 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
@@ -1,5 +1,3 @@
defmodule BlockScoutWeb.AddressTokenView do
use BlockScoutWeb, :view
-
- alias Explorer.Chain.Address
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
index 64ba591cec..2985d42b2c 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
@@ -2,7 +2,6 @@ defmodule BlockScoutWeb.AddressTransactionView do
use BlockScoutWeb, :view
alias BlockScoutWeb.AccessHelpers
- alias Explorer.Chain.Address
def format_current_filter(filter) do
case filter do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
index a29b0e7796..9ccab7c9d1 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
@@ -7,6 +7,11 @@ defmodule BlockScoutWeb.API.RPC.TokenView do
RPCView.render("show.json", data: prepare_token(token))
end
+ def render("gettokenholders.json", %{token_holders: token_holders}) do
+ data = Enum.map(token_holders, &prepare_token_holder/1)
+ RPCView.render("show.json", data: data)
+ end
+
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
@@ -22,4 +27,11 @@ defmodule BlockScoutWeb.API.RPC.TokenView do
"cataloged" => token.cataloged
}
end
+
+ defp prepare_token_holder(token_holder) do
+ %{
+ "address" => to_string(token_holder.address_hash),
+ "value" => token_holder.value
+ }
+ end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
index a993801240..fd75ee25df 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
@@ -2,6 +2,7 @@ defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view
alias Explorer.Chain
+ alias Explorer.Chain.Hash.Address
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
@@ -41,46 +42,256 @@ defmodule BlockScoutWeb.SmartContractView do
def named_argument?(%{"name" => _}), do: true
def named_argument?(_), do: false
- def values(addresses, type) when is_list(addresses) and type == "address[]" do
- addresses
- |> Enum.map(&values(&1, "address"))
- |> Enum.join(", ")
+ def values_with_type(value, type, components \\ nil)
+
+ def values_with_type(value, type, components) when is_list(value) do
+ cond do
+ String.starts_with?(type, "tuple") ->
+ tuple_types =
+ type
+ |> String.slice(0..-3)
+ |> supplement_type_with_components(components)
+
+ values =
+ value
+ |> tuple_array_to_array(tuple_types)
+ |> Enum.join(", ")
+
+ render_array_type_value(type, values)
+
+ String.starts_with?(type, "address") ->
+ values =
+ value
+ |> Enum.map(&binary_to_utf_string(&1))
+ |> Enum.join(", ")
+
+ render_array_type_value(type, values)
+
+ String.starts_with?(type, "bytes") ->
+ values =
+ value
+ |> Enum.map(&binary_to_utf_string(&1))
+ |> Enum.join(", ")
+
+ render_array_type_value(type, values)
+
+ true ->
+ values =
+ value
+ |> Enum.join(", ")
+
+ render_array_type_value(type, values)
+ end
end
- def values(values, type) when is_list(values) and type == "tuple[]" do
- array_from_tuple = tuple_array_to_array(values)
+ def values_with_type(value, type, _components) when is_tuple(value) do
+ values =
+ value
+ |> tuple_to_array(type)
+ |> Enum.join(", ")
- array_from_tuple
- |> Enum.join(", ")
+ render_type_value(type, values)
end
- def values(value, _type) when is_tuple(value) do
- tuple_to_array(value)
+ def values_with_type(value, type, _components) when type in [:address, "address", "address payable"] do
+ {:ok, address} = Address.cast(value)
+ render_type_value("address", to_string(address))
end
- def values(value, type) when type in ["address", "address payable"] do
- {:ok, address} = Explorer.Chain.Hash.Address.cast(value)
- to_string(address)
+ def values_with_type(value, "string", _components), do: render_type_value("string", value)
+
+ def values_with_type(value, "bool", _components), do: render_type_value("bool", to_string(value))
+
+ def values_with_type(value, type, _components) do
+ if String.starts_with?(type, "uint") do
+ render_type_value(type, to_string(value))
+ else
+ render_type_value(type, binary_to_utf_string(value))
+ end
+ end
+
+ def values_only(value, type, components) when is_list(value) do
+ cond do
+ String.starts_with?(type, "tuple") ->
+ tuple_types =
+ type
+ |> String.slice(0..-3)
+ |> supplement_type_with_components(components)
+
+ values =
+ value
+ |> tuple_array_to_array(tuple_types)
+ |> Enum.join(", ")
+
+ render_array_value(values)
+
+ String.starts_with?(type, "address") ->
+ values =
+ value
+ |> Enum.map(&binary_to_utf_string(&1))
+ |> Enum.join(", ")
+
+ render_array_value(values)
+
+ String.starts_with?(type, "bytes") ->
+ values =
+ value
+ |> Enum.map(&binary_to_utf_string(&1))
+ |> Enum.join(", ")
+
+ render_array_value(values)
+
+ true ->
+ values =
+ value
+ |> Enum.join(", ")
+
+ render_array_value(values)
+ end
end
- def values(values, _) when is_list(values), do: Enum.join(values, ",")
- def values(value, _), do: value
+ def values_only(value, type, _components) when is_tuple(value) do
+ values =
+ value
+ |> tuple_to_array(type)
+ |> Enum.join(", ")
- defp tuple_array_to_array(values) do
values
- |> Enum.map(fn value ->
- tuple_to_array(value)
- end)
end
- defp tuple_to_array(value) do
+ def values_only(value, type, _components) when type in [:address, "address", "address payable"] do
+ {:ok, address} = Address.cast(value)
+ to_string(address)
+ end
+
+ def values_only(value, "string", _components), do: value
+
+ def values_only(value, "bool", _components), do: to_string(value)
+
+ def values_only(value, type, _components) do
+ if String.starts_with?(type, "uint") do
+ to_string(value)
+ else
+ binary_to_utf_string(value)
+ end
+ end
+
+ defp tuple_array_to_array(value, type) do
value
- |> Tuple.to_list()
- |> Enum.map(&binary_to_utf_string(&1))
- |> Enum.join(",")
+ |> Enum.map(fn item ->
+ tuple_to_array(item, type)
+ end)
+ end
+
+ defp tuple_to_array(value, type) do
+ types_string =
+ type
+ |> String.slice(6..-2)
+
+ types =
+ if String.trim(types_string) == "" do
+ []
+ else
+ types_string
+ |> String.split(",")
+ end
+
+ {tuple_types, _} =
+ types
+ |> Enum.reduce({[], nil}, fn val, acc ->
+ {arr, to_merge} = acc
+
+ if to_merge do
+ if count_string_symbols(val)["]"] > count_string_symbols(val)["["] do
+ updated_arr = update_last_list_item(arr, val)
+ {updated_arr, !to_merge}
+ else
+ updated_arr = update_last_list_item(arr, val)
+ {updated_arr, to_merge}
+ end
+ else
+ if count_string_symbols(val)["["] > count_string_symbols(val)["]"] do
+ # credo:disable-for-next-line
+ {arr ++ [val], !to_merge}
+ else
+ # credo:disable-for-next-line
+ {arr ++ [val], to_merge}
+ end
+ end
+ end)
+
+ values_list =
+ value
+ |> Tuple.to_list()
+
+ values_types_list = Enum.zip(tuple_types, values_list)
+
+ values_types_list
+ |> Enum.map(fn {type, value} ->
+ values_with_type(value, type)
+ end)
+ end
+
+ defp update_last_list_item(arr, new_val) do
+ arr
+ |> Enum.with_index()
+ |> Enum.map(fn {item, index} ->
+ if index == Enum.count(arr) - 1 do
+ item <> "," <> new_val
+ else
+ item
+ end
+ end)
+ end
+
+ defp count_string_symbols(str) do
+ str
+ |> String.graphemes()
+ |> Enum.reduce(%{"[" => 0, "]" => 0}, fn char, acc ->
+ Map.update(acc, char, 1, &(&1 + 1))
+ end)
end
defp binary_to_utf_string(item) do
- if is_binary(item), do: "0x" <> Base.encode16(item, case: :lower), else: item
+ if is_binary(item) do
+ if String.starts_with?(item, "0x") do
+ item
+ else
+ "0x" <> Base.encode16(item, case: :lower)
+ end
+ else
+ item
+ end
+ end
+
+ defp render_type_value(type, value) do
+ "
(#{type}) : #{value}
"
+ end
+
+ defp render_array_type_value(type, values) do
+ value_to_display = "[" <> values <> "]"
+
+ render_type_value(type, value_to_display)
+ end
+
+ defp render_array_value(values) do
+ value_to_display = "[" <> values <> "]"
+
+ value_to_display
+ end
+
+ defp supplement_type_with_components(type, components) do
+ if type == "tuple" && components do
+ types =
+ components
+ |> Enum.map(fn component ->
+ Map.get(component, "type")
+ end)
+ |> Enum.join(",")
+
+ "tuple[" <> types <> "]"
+ else
+ type
+ end
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex
index b97d764b5e..91360fe475 100644
--- a/apps/block_scout_web/lib/block_scout_web/web_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex
@@ -256,12 +256,8 @@ defmodule BlockScoutWeb.WebRouter do
get("/search-logs", AddressLogsController, :search_logs)
- get("/transactions_csv", AddressTransactionController, :transactions_csv)
-
get("/token-autocomplete", ChainController, :token_autocomplete)
- get("/token-transfers-csv", AddressTransactionController, :token_transfers_csv)
-
get("/chain-blocks", ChainController, :chain_blocks, as: :chain_blocks)
get("/token-counters", Tokens.TokenController, :token_counters)
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index f9fdbdeede..a597bd0776 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -141,7 +141,7 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:91
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
-#: lib/block_scout_web/views/address_transaction_view.ex:11
+#: lib/block_scout_web/views/address_transaction_view.ex:10
msgid "All"
msgstr ""
@@ -252,12 +252,6 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr ""
-#, elixir-format
-#: lib/block_scout_web/templates/address_token/index.html.eex:30
-#: lib/block_scout_web/templates/address_transaction/index.html.eex:63
-msgid "CSV"
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/views/internal_transaction_view.ex:21
msgid "Call"
@@ -706,7 +700,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:36
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9
-#: lib/block_scout_web/views/address_transaction_view.ex:10
+#: lib/block_scout_web/views/address_transaction_view.ex:9
msgid "From"
msgstr ""
@@ -856,7 +850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:30
#: lib/block_scout_web/views/address_internal_transaction_view.ex:8
#: lib/block_scout_web/views/address_token_transfer_view.ex:8
-#: lib/block_scout_web/views/address_transaction_view.ex:9
+#: lib/block_scout_web/views/address_transaction_view.ex:8
msgid "To"
msgstr ""
@@ -2132,18 +2126,6 @@ msgstr ""
msgid "Claim the Amount"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
-msgid "Current Reward Percent"
-msgstr ""
-
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
-msgid "Current Reward Percent is calculated based on the Working Stake Amount."
-msgstr ""
-
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32
@@ -2162,7 +2144,6 @@ msgid "DApp for Staking %{symbol} tokens"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
#: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31
#: lib/block_scout_web/templates/stakes/_table.html.eex:38
msgid "Delegators"
@@ -2290,13 +2271,6 @@ msgstr ""
msgid "Pools searching is already in progress for this address"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
-msgid "Potential Reward Percent"
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16
msgid "Reason for Ban: %{ban_reason}"
@@ -2323,12 +2297,6 @@ msgstr ""
msgid "Reward calculating is already in progress for this address"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
-msgid "Reward distribution is based on stake amount. Validator receives a minimum of %{min}%."
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9
msgid "Searching for pools you have ever staked into. Please, wait..."
@@ -2664,3 +2632,33 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:247
msgid "Press / and focus will be moved to the search field"
msgstr ""
+
+#, elixir-format
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
+msgid "Current Reward Share"
+msgstr ""
+
+#, elixir-format
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
+msgid "Current Reward Share is calculated based on the Working Stake Amount."
+msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
+msgid "Delegators of "
+msgstr ""
+
+#, elixir-format
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
+msgid "Potential Reward Share"
+msgstr ""
+
+#, elixir-format
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
+msgid "Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward."
+msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index f9fdbdeede..7543fa4c8b 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -141,7 +141,7 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:91
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
-#: lib/block_scout_web/views/address_transaction_view.ex:11
+#: lib/block_scout_web/views/address_transaction_view.ex:10
msgid "All"
msgstr ""
@@ -252,12 +252,6 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr ""
-#, elixir-format
-#: lib/block_scout_web/templates/address_token/index.html.eex:30
-#: lib/block_scout_web/templates/address_transaction/index.html.eex:63
-msgid "CSV"
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/views/internal_transaction_view.ex:21
msgid "Call"
@@ -706,7 +700,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:36
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9
-#: lib/block_scout_web/views/address_transaction_view.ex:10
+#: lib/block_scout_web/views/address_transaction_view.ex:9
msgid "From"
msgstr ""
@@ -856,7 +850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:30
#: lib/block_scout_web/views/address_internal_transaction_view.ex:8
#: lib/block_scout_web/views/address_token_transfer_view.ex:8
-#: lib/block_scout_web/views/address_transaction_view.ex:9
+#: lib/block_scout_web/views/address_transaction_view.ex:8
msgid "To"
msgstr ""
@@ -2132,18 +2126,6 @@ msgstr ""
msgid "Claim the Amount"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
-msgid "Current Reward Percent"
-msgstr ""
-
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
-msgid "Current Reward Percent is calculated based on the Working Stake Amount."
-msgstr ""
-
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32
@@ -2162,7 +2144,6 @@ msgid "DApp for Staking %{symbol} tokens"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
#: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31
#: lib/block_scout_web/templates/stakes/_table.html.eex:38
msgid "Delegators"
@@ -2290,13 +2271,6 @@ msgstr ""
msgid "Pools searching is already in progress for this address"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
-msgid "Potential Reward Percent"
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16
msgid "Reason for Ban: %{ban_reason}"
@@ -2323,12 +2297,6 @@ msgstr ""
msgid "Reward calculating is already in progress for this address"
msgstr ""
-#, elixir-format
-#:
-#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
-msgid "Reward distribution is based on stake amount. Validator receives a minimum of %{min}%."
-msgstr ""
-
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9
msgid "Searching for pools you have ever staked into. Please, wait..."
@@ -2664,3 +2632,33 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:247
msgid "Press / and focus will be moved to the search field"
msgstr ""
+
+#, elixir-format, fuzzy
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
+msgid "Current Reward Share"
+msgstr ""
+
+#, elixir-format, fuzzy
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
+msgid "Current Reward Share is calculated based on the Working Stake Amount."
+msgstr ""
+
+#, elixir-format, fuzzy
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
+msgid "Delegators of "
+msgstr ""
+
+#, elixir-format, fuzzy
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
+msgid "Potential Reward Share"
+msgstr ""
+
+#, elixir-format, fuzzy
+#:
+#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
+msgid "Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward."
+msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
index 6e523f8b42..fc56421061 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
@@ -157,40 +157,4 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
end)
end
end
-
- describe "GET token-transfers-csv/2" do
- test "exports token transfers to csv", %{conn: conn} do
- address = insert(:address)
-
- transaction =
- :transaction
- |> insert(from_address: address)
- |> with_block()
-
- insert(:token_transfer, transaction: transaction, from_address: address)
- insert(:token_transfer, transaction: transaction, to_address: address)
-
- conn = get(conn, "/token-transfers-csv", %{"address_id" => Address.checksum(address.hash)})
-
- assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
- end
- end
-
- describe "GET transactions_csv/2" do
- test "download csv file with transactions", %{conn: conn} do
- address = insert(:address)
-
- :transaction
- |> insert(from_address: address)
- |> with_block()
-
- :transaction
- |> insert(from_address: address)
- |> with_block()
-
- conn = get(conn, "/transactions_csv", %{"address_id" => Address.checksum(address.hash)})
-
- assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
- end
- end
end
diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
index 32be1fab47..66dd911eb9 100644
--- a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
@@ -240,21 +240,28 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end
end
- describe "values/2" do
+ describe "values_only/2" do
test "joins the values when it is a list of a given type" do
values = [8, 6, 9, 2, 2, 37]
- assert SmartContractView.values(values, "type") == "8,6,9,2,2,37"
+ assert SmartContractView.values_only(values, "type", nil) == "[8, 6, 9, 2, 2, 37]"
end
test "convert the value to string receiving a value and the 'address' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
- assert SmartContractView.values(value, "address") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
+ assert SmartContractView.values_only(value, "address", nil) == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
+ end
+
+ test "convert the value to string receiving a value and the :address type" do
+ value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
+ assert SmartContractView.values_only(value, :address, nil) == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end
test "convert the value to string receiving a value and the 'address payable' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
- assert SmartContractView.values(value, "address payable") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
+
+ assert SmartContractView.values_only(value, "address payable", nil) ==
+ "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end
test "convert each value to string and join them when receiving 'address[]' as the type" do
@@ -263,14 +270,41 @@ defmodule BlockScoutWeb.SmartContractViewTest do
<<207, 38, 14, 163, 23, 85, 86, 55, 197, 95, 112, 229, 93, 186, 141, 90, 216, 65, 76, 176>>
]
- assert SmartContractView.values(value, "address[]") ==
- "0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0"
+ assert SmartContractView.values_only(value, "address[]", nil) ==
+ "[0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0]"
end
test "returns the value when the type is neither 'address' nor 'address payable'" do
value = "POA"
- assert SmartContractView.values(value, "not address") == "POA"
+ assert SmartContractView.values_only(value, "string", nil) == "POA"
+ end
+
+ test "returns the value when the type is boolean" do
+ value = "true"
+
+ assert SmartContractView.values_only(value, "bool", nil) == "true"
+ end
+
+ test "returns the value when the type is bytes4" do
+ value = <<228, 184, 12, 77>>
+
+ assert SmartContractView.values_only(value, "bytes4", nil) == "0xe4b80c4d"
+ end
+
+ test "returns the value when the type is bytes32" do
+ value =
+ <<156, 209, 70, 119, 249, 170, 85, 105, 179, 187, 179, 81, 252, 214, 125, 17, 21, 170, 86, 58, 225, 98, 66, 118,
+ 211, 212, 230, 127, 179, 214, 249, 38>>
+
+ assert SmartContractView.values_only(value, "bytes32", nil) ==
+ "0x9cd14677f9aa5569b3bbb351fcd67d1115aa563ae1624276d3d4e67fb3d6f926"
+ end
+
+ test "returns the value when the type is uint(n) and value is 0" do
+ value = "0"
+
+ assert SmartContractView.values_only(value, "uint64", nil) == "0"
end
end
end
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs
index a6290bdfb2..e9bca62003 100644
--- a/apps/explorer/config/config.exs
+++ b/apps/explorer/config/config.exs
@@ -224,7 +224,9 @@ config :explorer, Explorer.Chain.Block.Reward,
if System.get_env("POS_STAKING_CONTRACT") do
config :explorer, Explorer.Staking.ContractState,
enabled: true,
- staking_contract_address: System.get_env("POS_STAKING_CONTRACT")
+ staking_contract_address: System.get_env("POS_STAKING_CONTRACT"),
+ eth_subscribe_max_delay: System.get_env("POS_ETH_SUBSCRIBE_MAX_DELAY", "60"),
+ eth_blocknumber_pull_interval: System.get_env("POS_ETH_BLOCKNUMBER_PULL_INTERVAL", "500")
else
config :explorer, Explorer.Staking.ContractState, enabled: false
end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 89e0e5d89d..4bcd063030 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -4601,7 +4601,7 @@ defmodule Explorer.Chain do
end
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
- def fetch_token_holders_from_token_hash(contract_address_hash, options) do
+ def fetch_token_holders_from_token_hash(contract_address_hash, options \\ []) do
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value(options)
|> Repo.all()
diff --git a/apps/explorer/lib/explorer/chain/address_token_transfer_csv_exporter.ex b/apps/explorer/lib/explorer/chain/address_token_transfer_csv_exporter.ex
deleted file mode 100644
index 184b391d9c..0000000000
--- a/apps/explorer/lib/explorer/chain/address_token_transfer_csv_exporter.ex
+++ /dev/null
@@ -1,119 +0,0 @@
-defmodule Explorer.Chain.AddressTokenTransferCsvExporter do
- @moduledoc """
- Exports token transfers to a csv file.
- """
-
- alias Explorer.{Chain, PagingOptions}
- alias Explorer.Chain.{TokenTransfer, Transaction}
- alias NimbleCSV.RFC4180
-
- @necessity_by_association [
- necessity_by_association: %{
- [created_contract_address: :names] => :optional,
- [from_address: :names] => :optional,
- [to_address: :names] => :optional,
- [token_transfers: :token] => :optional,
- [token_transfers: :to_address] => :optional,
- [token_transfers: :from_address] => :optional,
- [token_transfers: :token_contract_address] => :optional,
- :block => :required
- }
- ]
-
- @page_size 150
- @paging_options %PagingOptions{page_size: @page_size + 1}
-
- def export(address) do
- address.hash
- |> fetch_all_transactions(@paging_options)
- |> to_token_transfers()
- |> to_csv_format(address)
- |> dump_to_stream()
- end
-
- defp fetch_all_transactions(address_hash, paging_options, acc \\ []) do
- options = Keyword.merge(@necessity_by_association, paging_options: paging_options)
-
- transactions =
- address_hash
- |> Chain.address_to_mined_transactions_with_rewards(options)
- |> Enum.filter(fn transaction -> Enum.count(transaction.token_transfers) > 0 end)
-
- new_acc = transactions ++ acc
-
- case Enum.split(transactions, @page_size) do
- {_transactions, [%Transaction{block_number: block_number, index: index}]} ->
- new_paging_options = %{@paging_options | key: {block_number, index}}
- fetch_all_transactions(address_hash, new_paging_options, new_acc)
-
- {_, []} ->
- new_acc
- end
- end
-
- defp to_token_transfers(transactions) do
- transactions
- |> Enum.flat_map(fn transaction ->
- transaction.token_transfers
- |> Enum.map(fn transfer -> %{transfer | transaction: transaction} end)
- end)
- end
-
- defp dump_to_stream(transactions) do
- transactions
- |> RFC4180.dump_to_stream()
- end
-
- defp to_csv_format(token_transfers, address) do
- row_names = [
- "TxHash",
- "BlockNumber",
- "UnixTimestamp",
- "FromAddress",
- "ToAddress",
- "TokenContractAddress",
- "Type",
- "TokenSymbol",
- "TokensTransferred",
- "TransactionFee",
- "Status",
- "ErrCode"
- ]
-
- token_transfer_lists =
- token_transfers
- |> Stream.map(fn token_transfer ->
- [
- to_string(token_transfer.transaction_hash),
- token_transfer.transaction.block_number,
- token_transfer.transaction.block.timestamp,
- token_transfer.from_address |> to_string() |> String.downcase(),
- token_transfer.to_address |> to_string() |> String.downcase(),
- token_transfer.token_contract_address |> to_string() |> String.downcase(),
- type(token_transfer, address.hash),
- token_transfer.token.symbol,
- token_transfer.amount,
- fee(token_transfer.transaction),
- token_transfer.transaction.status,
- token_transfer.transaction.error
- ]
- end)
-
- Stream.concat([row_names], token_transfer_lists)
- end
-
- defp type(%TokenTransfer{from_address_hash: address_hash}, address_hash), do: "OUT"
-
- defp type(%TokenTransfer{to_address_hash: address_hash}, address_hash), do: "IN"
-
- defp type(_, _), do: ""
-
- defp fee(transaction) do
- transaction
- |> Chain.fee(:wei)
- |> case do
- {:actual, value} -> value
- {:maximum, value} -> "Max of #{value}"
- end
- end
-end
diff --git a/apps/explorer/lib/explorer/chain/address_transaction_csv_exporter.ex b/apps/explorer/lib/explorer/chain/address_transaction_csv_exporter.ex
deleted file mode 100644
index d608cc55e3..0000000000
--- a/apps/explorer/lib/explorer/chain/address_transaction_csv_exporter.ex
+++ /dev/null
@@ -1,139 +0,0 @@
-defmodule Explorer.Chain.AddressTransactionCsvExporter do
- @moduledoc """
- Exports transactions to a csv file.
- """
-
- import Ecto.Query,
- only: [
- from: 2
- ]
-
- alias Explorer.{Chain, Market, PagingOptions, Repo}
- alias Explorer.Market.MarketHistory
- alias Explorer.Chain.{Address, Transaction, Wei}
- alias Explorer.ExchangeRates.Token
- alias NimbleCSV.RFC4180
-
- @necessity_by_association [
- necessity_by_association: %{
- [created_contract_address: :names] => :optional,
- [from_address: :names] => :optional,
- [to_address: :names] => :optional,
- [token_transfers: :token] => :optional,
- [token_transfers: :to_address] => :optional,
- [token_transfers: :from_address] => :optional,
- [token_transfers: :token_contract_address] => :optional,
- :block => :required
- }
- ]
-
- @page_size 150
-
- @paging_options %PagingOptions{page_size: @page_size + 1}
-
- @spec export(Address.t()) :: Enumerable.t()
- def export(address) do
- exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
-
- address.hash
- |> fetch_all_transactions(@paging_options)
- |> to_csv_format(address, exchange_rate)
- |> dump_to_stream()
- end
-
- defp fetch_all_transactions(address_hash, paging_options, acc \\ []) do
- options = Keyword.put(@necessity_by_association, :paging_options, paging_options)
-
- transactions = Chain.address_to_transactions_without_rewards(address_hash, options)
-
- new_acc = transactions ++ acc
-
- case Enum.split(transactions, @page_size) do
- {_transactions, [%Transaction{block_number: block_number, index: index}]} ->
- new_paging_options = %{@paging_options | key: {block_number, index}}
- fetch_all_transactions(address_hash, new_paging_options, new_acc)
-
- {_, []} ->
- new_acc
- end
- end
-
- defp dump_to_stream(transactions) do
- transactions
- |> RFC4180.dump_to_stream()
- end
-
- defp to_csv_format(transactions, address, exchange_rate) do
- row_names = [
- "TxHash",
- "BlockNumber",
- "UnixTimestamp",
- "FromAddress",
- "ToAddress",
- "ContractAddress",
- "Type",
- "Value",
- "Fee",
- "Status",
- "ErrCode",
- "CurrentPrice",
- "TxDateOpeningPrice",
- "TxDateClosingPrice"
- ]
-
- transaction_lists =
- transactions
- |> Stream.map(fn transaction ->
- {opening_price, closing_price} = price_at_date(transaction.block.timestamp)
-
- [
- to_string(transaction.hash),
- transaction.block_number,
- transaction.block.timestamp,
- to_string(transaction.from_address),
- to_string(transaction.to_address),
- to_string(transaction.created_contract_address),
- type(transaction, address.hash),
- Wei.to(transaction.value, :wei),
- fee(transaction),
- transaction.status,
- transaction.error,
- exchange_rate.usd_value,
- opening_price,
- closing_price
- ]
- end)
-
- Stream.concat([row_names], transaction_lists)
- end
-
- defp type(%Transaction{from_address_hash: address_hash}, address_hash), do: "OUT"
-
- defp type(%Transaction{to_address_hash: address_hash}, address_hash), do: "IN"
-
- defp type(_, _), do: ""
-
- defp fee(transaction) do
- transaction
- |> Chain.fee(:wei)
- |> case do
- {:actual, value} -> value
- {:maximum, value} -> "Max of #{value}"
- end
- end
-
- defp price_at_date(datetime) do
- date = DateTime.to_date(datetime)
-
- query =
- from(
- mh in MarketHistory,
- where: mh.date == ^date
- )
-
- case Repo.one(query) do
- nil -> {nil, nil}
- price -> {price.opening_price, price.closing_price}
- end
- end
-end
diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex
index c363e459d7..21e68ff827 100644
--- a/apps/explorer/lib/explorer/chain/events/subscriber.ex
+++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex
@@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a
- @allowed_events ~w(exchange_rate transaction_stats)a
+ @allowed_events ~w(exchange_rate stake_snapshotting_finished transaction_stats)a
@type broadcast_type :: :realtime | :catchup | :on_demand
diff --git a/apps/explorer/lib/explorer/staking/contract_reader.ex b/apps/explorer/lib/explorer/staking/contract_reader.ex
index 5307646c95..29e987930b 100644
--- a/apps/explorer/lib/explorer/staking/contract_reader.ex
+++ b/apps/explorer/lib/explorer/staking/contract_reader.ex
@@ -19,6 +19,8 @@ defmodule Explorer.Staking.ContractReader do
inactive_pools: {:staking, "df6f55f5", [], block_number},
# f0786096 = keccak256(MAX_CANDIDATES())
max_candidates: {:staking, "f0786096", [], block_number},
+ # 714897df = keccak256(MAX_VALIDATORS())
+ max_validators: {:validator_set, "714897df", [], block_number},
# 5fef7643 = keccak256(candidateMinStake())
min_candidate_stake: {:staking, "5fef7643", [], block_number},
# da7a9b6a = keccak256(delegatorMinStake())
@@ -29,6 +31,8 @@ defmodule Explorer.Staking.ContractReader do
pools_to_be_elected: {:staking, "a5d54f65", [], block_number},
# f4942501 = keccak256(areStakeAndWithdrawAllowed())
staking_allowed: {:staking, "f4942501", [], block_number},
+ # 74bdb372 = keccak256(lastChangeBlock())
+ staking_last_change_block: {:staking, "74bdb372", [], block_number},
# 2d21d217 = keccak256(erc677TokenContract())
token_contract_address: {:staking, "2d21d217", [], block_number},
# 704189ca = keccak256(unremovableValidator())
@@ -36,7 +40,9 @@ defmodule Explorer.Staking.ContractReader do
# b7ab4db5 = keccak256(getValidators())
validators: {:validator_set, "b7ab4db5", [], block_number},
# b927ef43 = keccak256(validatorSetApplyBlock())
- validator_set_apply_block: {:validator_set, "b927ef43", [], block_number}
+ validator_set_apply_block: {:validator_set, "b927ef43", [], block_number},
+ # 74bdb372 = keccak256(lastChangeBlock())
+ validator_set_last_change_block: {:validator_set, "74bdb372", [], block_number}
]
end
diff --git a/apps/explorer/lib/explorer/staking/contract_state.ex b/apps/explorer/lib/explorer/staking/contract_state.ex
index c41e1cf60b..83dfdddaef 100644
--- a/apps/explorer/lib/explorer/staking/contract_state.ex
+++ b/apps/explorer/lib/explorer/staking/contract_state.ex
@@ -23,9 +23,11 @@ defmodule Explorer.Staking.ContractState do
:epoch_number,
:epoch_start_block,
:is_snapshotting,
+ :last_change_block,
:max_candidates,
:min_candidate_stake,
:min_delegator_stake,
+ :seen_block,
:snapshotted_epoch_number,
:staking_allowed,
:staking_contract,
@@ -36,11 +38,14 @@ defmodule Explorer.Staking.ContractState do
:validator_set_contract
]
- # frequency in blocks
+ # token renewal frequency in blocks
@token_renew_frequency 10
defstruct [
- :seen_block,
+ :eth_blocknumber_pull_interval,
+ :eth_subscribe_max_delay,
+ :snapshotting_finished,
+ :timer,
:contracts,
:abi
]
@@ -69,13 +74,22 @@ defmodule Explorer.Staking.ContractState do
])
Subscriber.to(:last_block_number, :realtime)
+ Subscriber.to(:stake_snapshotting_finished)
staking_abi = abi("StakingAuRa")
validator_set_abi = abi("ValidatorSetAuRa")
block_reward_abi = abi("BlockRewardAuRa")
token_abi = abi("Token")
- staking_contract_address = Application.get_env(:explorer, __MODULE__)[:staking_contract_address]
+ module_env = Application.get_env(:explorer, __MODULE__)
+
+ # eth_blockNumber pull interval, in milliseconds
+ eth_blocknumber_pull_interval = String.to_integer(module_env[:eth_blocknumber_pull_interval])
+
+ # eth_subscribe max delay to switch to eth_blockNumber, in seconds
+ eth_subscribe_max_delay = String.to_integer(module_env[:eth_subscribe_max_delay])
+
+ staking_contract_address = module_env[:staking_contract_address]
# 2d21d217 = keccak256(erc677TokenContract())
erc_677_token_contract_signature = "2d21d217"
@@ -100,7 +114,10 @@ defmodule Explorer.Staking.ContractState do
})
state = %__MODULE__{
- seen_block: 0,
+ eth_blocknumber_pull_interval: eth_blocknumber_pull_interval,
+ eth_subscribe_max_delay: eth_subscribe_max_delay,
+ snapshotting_finished: false,
+ timer: nil,
contracts: %{
staking: staking_contract_address,
validator_set: validator_set_contract_address,
@@ -112,6 +129,8 @@ defmodule Explorer.Staking.ContractState do
:ets.insert(@table_name,
block_reward_contract: %{abi: block_reward_abi, address: block_reward_contract_address},
is_snapshotting: false,
+ last_change_block: 0,
+ seen_block: 0,
snapshotted_epoch_number: -1,
staking_contract: %{abi: staking_abi, address: staking_contract_address},
token_contract: %{abi: token_abi, address: token_contract_address},
@@ -123,58 +142,135 @@ defmodule Explorer.Staking.ContractState do
end
def handle_continue(_, state) do
+ # if eth_subscribe doesn't work during the first eth_subscribe_max_delay seconds
+ # after server start, use eth_blockNumber
+ timer = Process.send_after(self(), :eth_subscribe_stopped, state.eth_subscribe_max_delay * 1000)
+ {:noreply, %{state | timer: timer}}
+ end
+
+ # handles an event about snapshotting finishing
+ def handle_info({:chain_event, :stake_snapshotting_finished}, state) do
+ {:noreply, %{state | snapshotting_finished: true}}
+ end
+
+ # received when eth_subscribe is stopped
+ def handle_info(:eth_subscribe_stopped, state) do
+ state =
+ if Process.read_timer(state.timer) == false do
+ {microseconds, state} =
+ :timer.tc(
+ fn st -> fetch_state(st, get_current_block_number()) end,
+ [state]
+ )
+
+ # sleep up to eth_blocknumber_pull_interval milliseconds before the next eth_blockNumber request
+ Process.send_after(
+ self(),
+ :eth_subscribe_stopped,
+ max(state.eth_blocknumber_pull_interval - round(microseconds / 1000), 0)
+ )
+
+ state
+ else
+ state
+ end
+
{:noreply, state}
end
- @doc "Handles new blocks and decides to fetch fresh chain info"
+ # catches a new block number from eth_subscribe
def handle_info({:chain_event, :last_block_number, :realtime, block_number}, state) do
- if block_number > state.seen_block do
- # read general info from the contracts (including pool list and validator list)
- global_responses =
- ContractReader.perform_requests(ContractReader.global_requests(block_number), state.contracts, state.abi)
-
- epoch_very_beginning = global_responses.epoch_start_block == block_number + 1
-
- if global_responses.epoch_number > get(:epoch_number) and not epoch_very_beginning and state.seen_block > 0 do
- # if the previous staking epoch finished and we have blocks gap,
- # call fetch_state in a loop until the blocks gap is closed
- loop_block_start = state.seen_block + 1
- loop_block_end = block_number - 1
-
- if loop_block_end >= loop_block_start do
- for bn <- loop_block_start..loop_block_end do
- gr = ContractReader.perform_requests(ContractReader.global_requests(bn), state.contracts, state.abi)
- fetch_state(state.contracts, state.abi, gr, bn, gr.epoch_start_block == bn + 1)
- end
- end
- end
+ if state.timer != nil do
+ Process.cancel_timer(state.timer)
+ end
- fetch_state(state.contracts, state.abi, global_responses, block_number, epoch_very_beginning)
- {:noreply, %{state | seen_block: block_number}}
+ state = fetch_state(state, block_number)
+
+ timer = Process.send_after(self(), :eth_subscribe_stopped, state.eth_subscribe_max_delay * 1000)
+ {:noreply, %{state | timer: timer}}
+ end
+
+ # handles new block and decides to fetch fresh chain info
+ defp fetch_state(state, block_number) do
+ if block_number <= get(:seen_block) do
+ state
else
- {:noreply, state}
+ fetch_state_internal(state, block_number)
end
end
- defp fetch_state(contracts, abi, global_responses, block_number, epoch_very_beginning) do
- validator_min_reward_percent =
- get_validator_min_reward_percent(global_responses.epoch_number, block_number, contracts, abi)
+ defp fetch_state_internal(state, block_number) do
+ # read general info from the contracts (including pool list and validator list)
+ global_responses =
+ ContractReader.perform_requests(ContractReader.global_requests(block_number), state.contracts, state.abi)
- is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true})
+ epoch_very_beginning = global_responses.epoch_start_block == block_number + 1
+
+ start_snapshotting = start_snapshotting?(global_responses)
- start_snapshotting =
- global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 &&
- not get(:is_snapshotting)
+ # determine if something changed in contracts state since the previous seen block.
+ # if something changed or the `fetch_state` function is called for the first time
+ # or we are at the beginning of staking epoch or snapshotting recently finished
+ # then we should update database
+ last_change_block =
+ max(global_responses.staking_last_change_block, global_responses.validator_set_last_change_block)
- active_pools_length = Enum.count(global_responses.active_pools)
+ first_fetch = get(:epoch_end_block, 0) == 0
+
+ should_update_db =
+ start_snapshotting or state.snapshotting_finished or first_fetch or last_change_block > get(:last_change_block)
# save the general info to ETS (excluding pool list and validator list)
- settings =
- global_responses
- |> get_settings(validator_min_reward_percent, block_number)
- |> Enum.concat(active_pools_length: active_pools_length)
+ set_settings(global_responses, state, block_number, last_change_block)
- :ets.insert(@table_name, settings)
+ if epoch_very_beginning or start_snapshotting do
+ # if the block_number is the latest block of the finished staking epoch
+ # or we are starting Blockscout server, the BlockRewardAuRa contract balance
+ # could increase before (without Mint/Transfer events),
+ # so we need to update its balance in database
+ update_block_reward_balance(block_number)
+ end
+
+ # we should update database as something changed in contracts state
+ if should_update_db do
+ update_database(
+ epoch_very_beginning,
+ start_snapshotting,
+ global_responses,
+ state.contracts,
+ state.abi,
+ block_number
+ )
+ end
+
+ # notify the UI about a new block
+ data = %{
+ active_pools_length: get(:active_pools_length),
+ block_number: block_number,
+ epoch_end_block: global_responses.epoch_end_block,
+ epoch_number: global_responses.epoch_number,
+ max_candidates: global_responses.max_candidates,
+ staking_allowed: global_responses.staking_allowed,
+ staking_token_defined: get(:token, nil) != nil,
+ validator_set_apply_block: global_responses.validator_set_apply_block
+ }
+
+ Publisher.broadcast([{:staking_update, data}], :realtime)
+
+ if state.snapshotting_finished do
+ %{state | snapshotting_finished: false}
+ else
+ state
+ end
+ end
+
+ defp start_snapshotting?(global_responses) do
+ global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 &&
+ not get(:is_snapshotting)
+ end
+
+ defp update_database(epoch_very_beginning, start_snapshotting, global_responses, contracts, abi, block_number) do
+ is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true})
# form the list of validator pools
validators =
@@ -220,14 +316,7 @@ defmodule Explorer.Staking.ContractState do
# call `BlockReward.delegatorShare` function for each delegator
# to get their reward share of the pool (needed for the `Delegators` list in UI)
- delegator_responses =
- Enum.reduce(staker_responses, %{}, fn {{pool_staking_address, staker_address, _is_active} = key, value}, acc ->
- if pool_staking_address != staker_address do
- Map.put(acc, key, value)
- else
- acc
- end
- end)
+ delegator_responses = get_delegator_responses(staker_responses)
delegator_reward_responses =
get_delegator_reward_responses(
@@ -239,9 +328,6 @@ defmodule Explorer.Staking.ContractState do
block_number
)
- # calculate total amount staked into all active pools
- staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount)
-
# calculate likelihood of becoming a validator on the next epoch
[likelihood_values, total_likelihood] = global_responses.pools_likelihood
# array of pool addresses (staking addresses)
@@ -252,7 +338,20 @@ defmodule Explorer.Staking.ContractState do
snapshotted_epoch_number = get(:snapshotted_epoch_number)
- # form entries for writing to the `staking_pools` table in DB
+ # form entries for writing to the `staking_pools_delegators` table in DB
+ delegator_entries = get_delegator_entries(staker_responses, delegator_reward_responses)
+
+ # perform SQL queries
+ {:ok, _} =
+ Chain.import(%{
+ staking_pools_delegators: %{params: delegator_entries},
+ timeout: :infinity
+ })
+
+ # form entries for writing to the `staking_pools` table in DB.
+ # !!! it's important to do this AFTER updating `staking_pools_delegators`
+ # !!! table because the `get_pool_entries` function requires fresh
+ # !!! info about delegators of validators from the `staking_pools_delegators` table
pool_entries =
get_pool_entries(%{
pools: pools,
@@ -263,25 +362,16 @@ defmodule Explorer.Staking.ContractState do
global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood,
- total_likelihood: total_likelihood,
- staked_total: staked_total
+ total_likelihood: total_likelihood
})
- # form entries for writing to the `staking_pools_delegators` table in DB
- delegator_entries = get_delegator_entries(staker_responses, delegator_reward_responses)
-
# perform SQL queries
{:ok, _} =
Chain.import(%{
staking_pools: %{params: pool_entries},
- staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity
})
- if epoch_very_beginning or start_snapshotting do
- at_start_snapshotting(block_number)
- end
-
if start_snapshotting do
do_start_snapshotting(
epoch_very_beginning,
@@ -293,23 +383,31 @@ defmodule Explorer.Staking.ContractState do
mining_to_staking_address
)
end
+ end
- # notify the UI about a new block
- data = %{
- active_pools_length: active_pools_length,
- block_number: block_number,
- epoch_end_block: global_responses.epoch_end_block,
- epoch_number: global_responses.epoch_number,
- max_candidates: global_responses.max_candidates,
- staking_allowed: global_responses.staking_allowed,
- staking_token_defined: get(:token, nil) != nil,
- validator_set_apply_block: global_responses.validator_set_apply_block
- }
+ defp get_current_block_number do
+ json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
- Publisher.broadcast([{:staking_update, data}], :realtime)
+ result =
+ %{id: 0, method: "eth_blockNumber", params: []}
+ |> EthereumJSONRPC.request()
+ |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
+
+ case result do
+ {:ok, response} ->
+ response
+ |> String.replace_leading("0x", "")
+ |> String.to_integer(16)
+
+ _ ->
+ 0
+ end
end
- defp get_settings(global_responses, validator_min_reward_percent, block_number) do
+ defp get_settings(global_responses, state, block_number) do
+ validator_min_reward_percent =
+ get_validator_min_reward_percent(global_responses.epoch_number, block_number, state.contracts, state.abi)
+
base_settings = get_base_settings(global_responses, validator_min_reward_percent)
update_token =
@@ -324,6 +422,17 @@ defmodule Explorer.Staking.ContractState do
end
end
+ defp set_settings(global_responses, state, block_number, last_change_block) do
+ settings =
+ global_responses
+ |> get_settings(state, block_number)
+ |> Enum.concat(active_pools_length: Enum.count(global_responses.active_pools))
+ |> Enum.concat(last_change_block: last_change_block)
+ |> Enum.concat(seen_block: block_number)
+
+ :ets.insert(@table_name, settings)
+ end
+
defp get_mining_to_staking_address(validators, contracts, abi, block_number) do
validators.all
|> Enum.map(&ContractReader.staking_by_mining_request(&1, block_number))
@@ -390,6 +499,16 @@ defmodule Explorer.Staking.ContractState do
|> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi)
end
+ defp get_delegator_responses(staker_responses) do
+ Enum.reduce(staker_responses, %{}, fn {{pool_staking_address, staker_address, _is_active} = key, value}, acc ->
+ if pool_staking_address != staker_address do
+ Map.put(acc, key, value)
+ else
+ acc
+ end
+ end)
+ end
+
defp get_delegator_reward_responses(
delegator_responses,
pool_staking_responses,
@@ -543,9 +662,11 @@ defmodule Explorer.Staking.ContractState do
global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood,
- total_likelihood: total_likelihood,
- staked_total: staked_total
+ total_likelihood: total_likelihood
}) do
+ # total amount staked into all active pools
+ staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount)
+
Enum.map(pools, fn pool_staking_address ->
staking_resp = pool_staking_responses[pool_staking_address]
mining_resp = pool_mining_responses[pool_staking_address]
@@ -565,6 +686,15 @@ defmodule Explorer.Staking.ContractState do
0
end
+ is_unremovable = address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator
+
+ likelihood_value =
+ if get(:active_pools_length) > global_responses.max_validators and not is_unremovable do
+ ratio(likelihood[pool_staking_address] || 0, total_likelihood)
+ else
+ 100
+ end
+
%{
staking_address_hash: pool_staking_address,
delegators_count: delegators_count,
@@ -575,11 +705,11 @@ defmodule Explorer.Staking.ContractState do
0
end,
validator_reward_ratio: Float.floor(candidate_reward_resp.validator_share / 10_000, 2),
- likelihood: ratio(likelihood[pool_staking_address] || 0, total_likelihood),
+ likelihood: likelihood_value,
validator_reward_percent: staking_resp.validator_reward_percent / 10_000,
is_deleted: false,
is_validator: is_validator,
- is_unremovable: address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator,
+ is_unremovable: is_unremovable,
ban_reason: binary_to_string(mining_resp.ban_reason)
}
|> Map.merge(
@@ -603,7 +733,7 @@ defmodule Explorer.Staking.ContractState do
end)
end
- defp at_start_snapshotting(block_number) do
+ defp update_block_reward_balance(block_number) do
# update ERC balance of the BlockReward contract
token = get(:token)
diff --git a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex
index 4ce07417b7..751da24aa2 100644
--- a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex
+++ b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex
@@ -8,6 +8,7 @@ defmodule Explorer.Staking.StakeSnapshotting do
require Logger
alias Explorer.Chain
+ alias Explorer.Chain.Events.Publisher
alias Explorer.Chain.{StakingPool, StakingPoolsDelegator}
alias Explorer.Staking.ContractReader
@@ -194,6 +195,8 @@ defmodule Explorer.Staking.StakeSnapshotting do
end
:ets.insert(ets_table_name, is_snapshotting: false)
+
+ Publisher.broadcast(:stake_snapshotting_finished)
end
defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
diff --git a/apps/explorer/lib/explorer/validator/metadata_importer.ex b/apps/explorer/lib/explorer/validator/metadata_importer.ex
index 5c6757ffbd..8cc3792a77 100644
--- a/apps/explorer/lib/explorer/validator/metadata_importer.ex
+++ b/apps/explorer/lib/explorer/validator/metadata_importer.ex
@@ -9,7 +9,12 @@ defmodule Explorer.Validator.MetadataImporter do
def import_metadata(metadata_maps) do
# Enforce Name ShareLocks order (see docs: sharelocks.md)
- ordered_metadata_maps = Enum.sort_by(metadata_maps, &{&1.address_hash, &1.name})
+ ordered_metadata_maps =
+ metadata_maps
+ |> Enum.filter(fn metadata ->
+ String.trim(metadata.name) !== ""
+ end)
+ |> Enum.sort_by(&{&1.address_hash, &1.name})
Repo.transaction(fn -> Enum.each(ordered_metadata_maps, &upsert_validator_metadata(&1)) end)
end
diff --git a/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json b/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json
index bf9cebbb58..ad2599813c 100644
--- a/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json
+++ b/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json
@@ -1074,5 +1074,19 @@
"payable": false,
"stateMutability": "view",
"type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "lastChangeBlock",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
}
]
diff --git a/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json b/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json
index 7906f27317..0a3d2fc4a4 100644
--- a/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json
+++ b/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json
@@ -694,5 +694,19 @@
"payable": false,
"stateMutability": "view",
"type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "lastChangeBlock",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
}
]
diff --git a/apps/explorer/test/explorer/chain/address_token_transfer_csv_exporter_test.exs b/apps/explorer/test/explorer/chain/address_token_transfer_csv_exporter_test.exs
deleted file mode 100644
index 24bd9d7f04..0000000000
--- a/apps/explorer/test/explorer/chain/address_token_transfer_csv_exporter_test.exs
+++ /dev/null
@@ -1,72 +0,0 @@
-defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do
- use Explorer.DataCase
-
- alias Explorer.Chain.AddressTokenTransferCsvExporter
-
- describe "export/1" do
- test "exports token transfers to csv" do
- address = insert(:address)
-
- transaction =
- :transaction
- |> insert(from_address: address)
- |> with_block()
-
- token_transfer = insert(:token_transfer, transaction: transaction, from_address: address)
-
- [result] =
- address
- |> AddressTokenTransferCsvExporter.export()
- |> Enum.to_list()
- |> Enum.drop(1)
- |> Enum.map(fn [
- tx_hash,
- _,
- block_number,
- _,
- timestamp,
- _,
- from_address,
- _,
- to_address,
- _,
- token_contract_address,
- _,
- type,
- _,
- token_symbol,
- _,
- tokens_transferred,
- _,
- transaction_fee,
- _,
- status,
- _,
- err_code,
- _
- ] ->
- %{
- tx_hash: tx_hash,
- block_number: block_number,
- timestamp: timestamp,
- from_address: from_address,
- to_address: to_address,
- token_contract_address: token_contract_address,
- type: type,
- token_symbol: token_symbol,
- tokens_transferred: tokens_transferred,
- transaction_fee: transaction_fee,
- status: status,
- err_code: err_code
- }
- end)
-
- assert result.block_number == to_string(transaction.block_number)
- assert result.tx_hash == to_string(transaction.hash)
- assert result.from_address == token_transfer.from_address_hash |> to_string() |> String.downcase()
- assert result.to_address == token_transfer.to_address_hash |> to_string() |> String.downcase()
- assert result.timestamp == to_string(transaction.block.timestamp)
- assert result.type == "OUT"
- end
- end
-end
diff --git a/apps/explorer/test/explorer/chain/address_transaction_csv_exporter_test.exs b/apps/explorer/test/explorer/chain/address_transaction_csv_exporter_test.exs
deleted file mode 100644
index 1f722abf87..0000000000
--- a/apps/explorer/test/explorer/chain/address_transaction_csv_exporter_test.exs
+++ /dev/null
@@ -1,105 +0,0 @@
-defmodule Explorer.Chain.AddressTransactionCsvExporterTest do
- use Explorer.DataCase
-
- alias Explorer.Chain.{AddressTransactionCsvExporter, Wei}
-
- describe "export/1" do
- test "exports address transactions to csv" do
- address = insert(:address)
-
- transaction =
- :transaction
- |> insert(from_address: address)
- |> with_block()
- |> Repo.preload(:token_transfers)
-
- [result] =
- address
- |> AddressTransactionCsvExporter.export()
- |> Enum.to_list()
- |> Enum.drop(1)
- |> Enum.map(fn [
- hash,
- _,
- block_number,
- _,
- timestamp,
- _,
- from_address,
- _,
- to_address,
- _,
- created_address,
- _,
- type,
- _,
- value,
- _,
- fee,
- _,
- status,
- _,
- error,
- _,
- cur_price,
- _,
- op_price,
- _,
- cl_price,
- _
- ] ->
- %{
- hash: hash,
- block_number: block_number,
- timestamp: timestamp,
- from_address: from_address,
- to_address: to_address,
- created_address: created_address,
- type: type,
- value: value,
- fee: fee,
- status: status,
- error: error,
- current_price: cur_price,
- opening_price: op_price,
- closing_price: cl_price
- }
- end)
-
- assert result.block_number == to_string(transaction.block_number)
- assert result.timestamp
- assert result.created_address == to_string(transaction.created_contract_address_hash)
- assert result.from_address == to_string(transaction.from_address)
- assert result.to_address == to_string(transaction.to_address)
- assert result.hash == to_string(transaction.hash)
- assert result.type == "OUT"
- assert result.value == transaction.value |> Wei.to(:wei) |> to_string()
- assert result.fee
- assert result.status == to_string(transaction.status)
- assert result.error == to_string(transaction.error)
- assert result.current_price
- assert result.opening_price
- assert result.closing_price
- end
-
- test "fetches all transactions" do
- address = insert(:address)
-
- 1..200
- |> Enum.map(fn _ ->
- :transaction
- |> insert(from_address: address)
- |> with_block()
- end)
- |> Enum.count()
-
- result =
- address
- |> AddressTransactionCsvExporter.export()
- |> Enum.to_list()
- |> Enum.drop(1)
-
- assert Enum.count(result) == 200
- end
- end
-end
diff --git a/apps/explorer/test/explorer/staking/contract_state_test.exs b/apps/explorer/test/explorer/staking/contract_state_test.exs
index 0fcc564b18..72aa2163a8 100644
--- a/apps/explorer/test/explorer/staking/contract_state_test.exs
+++ b/apps/explorer/test/explorer/staking/contract_state_test.exs
@@ -25,7 +25,9 @@ defmodule Explorer.Staking.ContractStateTest do
Application.put_env(:explorer, ContractState,
enabled: true,
- staking_contract_address: "0x1100000000000000000000000000000000000001"
+ staking_contract_address: "0x1100000000000000000000000000000000000001",
+ eth_blocknumber_pull_interval: "500",
+ eth_subscribe_max_delay: "60"
)
start_supervised!(ContractState)
@@ -99,7 +101,7 @@ defmodule Explorer.Staking.ContractStateTest do
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
- assert length(requests) == 15
+ assert length(requests) == 18
{:ok,
format_responses([
@@ -114,24 +116,30 @@ defmodule Explorer.Staking.ContractStateTest do
# 5 StakingAuRa.getPoolsInactive
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
# 6 StakingAuRa.MAX_CANDIDATES
- "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000bb8",
- # 7 StakingAuRa.candidateMinStake
+ "0x0000000000000000000000000000000000000000000000000000000000000bb8",
+ # 7 StakingAuRa.MAX_VALIDATORS
+ "0x0000000000000000000000000000000000000000000000000000000000000013",
+ # 8 StakingAuRa.candidateMinStake
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
- # 8 StakingAuRa.delegatorMinStake
+ # 9 StakingAuRa.delegatorMinStake
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
- # 9 StakingAuRa.getPoolsLikelihood
+ # 10 StakingAuRa.getPoolsLikelihood
"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000098a7d9b8314c000000000000000000000000000000000000000000000000000029a2241af62c0000",
- # 10 StakingAuRa.getPoolsToBeElected
+ # 11 StakingAuRa.getPoolsToBeElected
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000b916e7e1f4bcb13549602ed042d36746fd0d96c9000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000b6695f5c2e3f5eff8036b5f5f3a9d83a5310e51e",
- # 11 StakingAuRa.areStakeAndWithdrawAllowed
+ # 12 StakingAuRa.areStakeAndWithdrawAllowed
"0x0000000000000000000000000000000000000000000000000000000000000000",
- # 12 StakingAuRa.erc677TokenContract
+ # 13 StakingAuRa.lastChangeBlock
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ # 14 StakingAuRa.erc677TokenContract
"0x0000000000000000000000006f7a73c96bd56f8b0debc795511eda135e105ea3",
- # 13 ValidatorSetAuRa.unremovableValidator
+ # 15 ValidatorSetAuRa.unremovableValidator
"0x0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6",
- # 14 ValidatorSetAuRa.getValidators
+ # 16 ValidatorSetAuRa.getValidators
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c7800000000000000000000000075df42383afe6bf5194aa8fa0e9b3d5f9e869441000000000000000000000000522df396ae70a058bd69778408630fdb023389b2",
- # 15 ValidatorSetAuRa.validatorSetApplyBlock
+ # 17 ValidatorSetAuRa.validatorSetApplyBlock
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ # 18 ValidatorSetAuRa.lastChangeBlock
"0x0000000000000000000000000000000000000000000000000000000000000000"
])}
end
@@ -152,6 +160,44 @@ defmodule Explorer.Staking.ContractStateTest do
end
)
+ # invoke update_block_reward_balance()
+
+ ## BalanceReader.get_balances_of
+ expect(
+ EthereumJSONRPC.Mox,
+ :json_rpc,
+ fn requests, _opts ->
+ assert length(requests) == 1
+
+ {:ok,
+ format_responses([
+ # ERC677BridgeTokenRewardable.balanceOf(BlockRewardAuRa)
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ])}
+ end
+ )
+
+ ## MetadataRetriever.get_functions_of
+ expect(
+ EthereumJSONRPC.Mox,
+ :json_rpc,
+ fn requests, _opts ->
+ assert length(requests) == 4
+
+ {:ok,
+ format_responses([
+ # ERC677BridgeTokenRewardable.decimals
+ "0x0000000000000000000000000000000000000000000000000000000000000012",
+ # ERC677BridgeTokenRewardable.name
+ "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
+ # ERC677BridgeTokenRewardable.symbol
+ "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
+ # ERC677BridgeTokenRewardable.totalSupply
+ "0x000000000000000000000000000000000000000000000001f399b1438a100000"
+ ])}
+ end
+ )
+
# get_validators
expect(
EthereumJSONRPC.Mox,
@@ -644,44 +690,6 @@ defmodule Explorer.Staking.ContractStateTest do
end
)
- # invoke at_start_snapshotting()
-
- ## BalanceReader.get_balances_of
- expect(
- EthereumJSONRPC.Mox,
- :json_rpc,
- fn requests, _opts ->
- assert length(requests) == 1
-
- {:ok,
- format_responses([
- # ERC677BridgeTokenRewardable.balanceOf(BlockRewardAuRa)
- "0x0000000000000000000000000000000000000000000000000000000000000000"
- ])}
- end
- )
-
- ## MetadataRetriever.get_functions_of
- expect(
- EthereumJSONRPC.Mox,
- :json_rpc,
- fn requests, _opts ->
- assert length(requests) == 4
-
- {:ok,
- format_responses([
- # ERC677BridgeTokenRewardable.decimals
- "0x0000000000000000000000000000000000000000000000000000000000000012",
- # ERC677BridgeTokenRewardable.name
- "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
- # ERC677BridgeTokenRewardable.symbol
- "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
- # ERC677BridgeTokenRewardable.totalSupply
- "0x000000000000000000000000000000000000000000000001f399b1438a100000"
- ])}
- end
- )
-
# invoke do_snapshotting()
## 1 snapshotted_pool_amounts_requests
diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex
index f6ad8a304a..ac064f4878 100644
--- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex
+++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex
@@ -86,7 +86,11 @@ defmodule Indexer.Block.Realtime.Fetcher do
)
when is_binary(quantity) do
number = quantity_to_integer(quantity)
- Publisher.broadcast([{:last_block_number, number}], :realtime)
+
+ if number > 0 do
+ Publisher.broadcast([{:last_block_number, number}], :realtime)
+ end
+
# Subscriptions don't support getting all the blocks and transactions data,
# so we need to go back and get the full block
start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen)