diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 1324d90572..5d621ddb40 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -24,5 +24,5 @@ lib/explorer/exchange_rates/source.ex:104 lib/explorer/exchange_rates/source.ex:107 lib/explorer/smart_contract/verifier.ex:89 lib/block_scout_web/templates/address_contract/index.html.eex:118 -lib/explorer/staking/stake_snapshotting.ex:14: Function do_snapshotting/6 has no local return -lib/explorer/staking/stake_snapshotting.ex:183 +lib/explorer/staking/stake_snapshotting.ex:15: Function do_snapshotting/6 has no local return +lib/explorer/staking/stake_snapshotting.ex:184 diff --git a/CHANGELOG.md b/CHANGELOG.md index d774abc75d..c023b4e9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ ## Current ### Features +- [#3584](https://github.com/poanetwork/blockscout/pull/3584) - Token holders API endpoint - [#3564](https://github.com/poanetwork/blockscout/pull/3564) - Staking welcome message ### Fixes +- [#3600](https://github.com/poanetwork/blockscout/pull/3600) - Prevent update validator metadata with empty name from contract +- [#3592](https://github.com/poanetwork/blockscout/pull/3592), [#3601](https://github.com/poanetwork/blockscout/pull/3601) - Contract interaction: fix nested tuples in the output view, add formatting +- [#3583](https://github.com/poanetwork/blockscout/pull/3583) - Reduce RPC requests and DB changes by Staking DApp - [#3577](https://github.com/poanetwork/blockscout/pull/3577) - Eliminate GraphiQL page XSS attack ### Chore +- [#3585](https://github.com/poanetwork/blockscout/pull/3585) - Add autoswitching from eth_subscribe to eth_blockNumber in Staking DApp - [#3574](https://github.com/poanetwork/blockscout/pull/3574) - Correct UNI token price - [#3567](https://github.com/poanetwork/blockscout/pull/3567) - Force to show filter at the page where filtered items list is empty - [#3565](https://github.com/poanetwork/blockscout/pull/3565) - Staking dapp: unhealthy state alert message diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index f35d3b508b..010c6d441a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -9,7 +9,6 @@ defmodule BlockScoutWeb.AddressTransactionController do alias BlockScoutWeb.{AccessHelpers, TransactionView} alias Explorer.{Chain, Market} - alias Explorer.Chain.{AddressTokenTransferCsvExporter, AddressTransactionCsvExporter} alias Explorer.ExchangeRates.Token alias Indexer.Fetcher.CoinBalanceOnDemand alias Phoenix.View @@ -140,48 +139,4 @@ defmodule BlockScoutWeb.AddressTransactionController do end end end - - def token_transfers_csv(conn, %{"address_id" => address_hash_string}) when is_binary(address_hash_string) do - with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do - address - |> AddressTokenTransferCsvExporter.export() - |> Enum.into( - conn - |> put_resp_content_type("application/csv") - |> put_resp_header("content-disposition", "attachment; filename=token_transfers.csv") - |> send_chunked(200) - ) - else - :error -> - unprocessable_entity(conn) - - {:error, :not_found} -> - not_found(conn) - end - end - - def token_transfers_csv(conn, _), do: not_found(conn) - - def transactions_csv(conn, %{"address_id" => address_hash_string}) do - with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do - address - |> AddressTransactionCsvExporter.export() - |> Enum.into( - conn - |> put_resp_content_type("application/csv") - |> put_resp_header("content-disposition", "attachment; filename=transactions.csv") - |> send_chunked(200) - ) - else - :error -> - unprocessable_entity(conn) - - {:error, :not_found} -> - not_found(conn) - end - end - - def transactions_csv(conn, _), do: not_found(conn) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex index ee965fd9b9..e24caee0d1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex @@ -1,7 +1,8 @@ defmodule BlockScoutWeb.API.RPC.TokenController do use BlockScoutWeb, :controller - alias Explorer.Chain + alias BlockScoutWeb.API.RPC.Helpers + alias Explorer.{Chain, PagingOptions} def gettoken(conn, params) do with {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params), @@ -20,6 +21,34 @@ defmodule BlockScoutWeb.API.RPC.TokenController do end end + def gettokenholders(conn, params) do + with pagination_options <- Helpers.put_pagination_options(%{}, params), + {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params), + {:format, {:ok, address_hash}} <- to_address_hash(contractaddress_param) do + options_with_defaults = + pagination_options + |> Map.put_new(:page_number, 0) + |> Map.put_new(:page_size, 10) + + options = [ + paging_options: %PagingOptions{ + key: nil, + page_number: options_with_defaults.page_number, + page_size: options_with_defaults.page_size + } + ] + + token_holders = Chain.fetch_token_holders_from_token_hash(address_hash, options) + render(conn, "gettokenholders.json", %{token_holders: token_holders}) + else + {:contractaddress_param, :error} -> + render(conn, :error, error: "Query parameter contract address is required") + + {:format, :error} -> + render(conn, :error, error: "Invalid contract address hash") + end + end + defp fetch_contractaddress(params) do {:contractaddress_param, Map.fetch(params, "contractaddress")} end diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 2b2d28b85d..f761a8e766 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -276,6 +276,23 @@ defmodule BlockScoutWeb.Etherscan do "result" => nil } + @token_gettokenholders_example_value %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "address" => "0x0000000000000000000000000000000000000000", + "value" => "965208500001258757122850" + } + ] + } + + @token_gettokenholders_example_value_error %{ + "status" => "0", + "message" => "Invalid contract address format", + "result" => nil + } + @stats_tokensupply_example_value %{ "status" => "1", "message" => "OK", @@ -664,6 +681,18 @@ defmodule BlockScoutWeb.Etherscan do } } + @token_holder_details %{ + name: "Token holder Detail", + fields: %{ + address: @address_hash_type, + value: %{ + type: "value", + definition: "A nonnegative number used to identify the balance of the target token.", + example: ~s("1000000000000000000") + } + } + } + @address_balance %{ name: "AddressBalance", fields: %{ @@ -1825,6 +1854,56 @@ defmodule BlockScoutWeb.Etherscan do ] } + @token_gettokenholders_action %{ + name: "getTokenHolders", + description: "Get token holders by contract address.", + required_params: [ + %{ + key: "contractaddress", + placeholder: "contractAddressHash", + type: "string", + description: "A 160-bit code used for identifying contracts." + } + ], + optional_params: [ + %{ + key: "page", + type: "integer", + description: + "A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction." + }, + %{ + key: "offset", + type: "integer", + description: + "A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction." + } + ], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@token_gettokenholders_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "array", + array_type: @token_holder_details + } + } + } + }, + %{ + code: "200", + description: "error", + example_value: Jason.encode!(@token_gettokenholders_example_value_error) + } + ] + } + @stats_tokensupply_action %{ name: "tokensupply", description: @@ -2446,7 +2525,10 @@ defmodule BlockScoutWeb.Etherscan do @token_module %{ name: "token", - actions: [@token_gettoken_action] + actions: [ + @token_gettoken_action, + @token_gettokenholders_action + ] } @stats_module %{ diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex index c31e4a0a7b..d765ea3ff7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex @@ -26,13 +26,6 @@
-
- Download Address.checksum(@address.hash)}) %>><%= gettext("CSV") %> - - - - -
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 7f7a1b7db2..10d7611e62 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -59,15 +59,6 @@
-
- Download Address.checksum(@address.hash)}) %>><%= gettext("CSV") %> -
- - - -
-
-
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex index c2f47607c3..a6110cc3d3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex @@ -3,8 +3,8 @@ [ <%= @function_name %> method Response ] [<%= for item <- @outputs do %> -<%= if named_argument?(item) do %><%= item["name"] %> -<% end %> -(<%= item["type"] %>) : <%= values(item["value"], item["type"]) %><% end %>] +<%= if named_argument?(item) do %><%= item["name"] %><% end %> +<%= raw(values_with_type(item["value"], item["type"])) %> +<% end %>] diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex index b47bf9cdcb..e438a32387 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -121,7 +121,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash) <% else %> - <%= values(output["value"], output["type"]) %> + <%= raw(values_only(output["value"], output["type"], output["components"])) %> <% end %> <% end %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex index 67f81ff442..76ae952269 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex @@ -2,7 +2,7 @@ <%= render BlockScoutWeb.StakesView, "_learn-more.html", conn: @conn %> -<%= render BlockScoutWeb.StakesView, "_warning.html", conn: @conn %> +<%= # render BlockScoutWeb.StakesView, "_warning.html", conn: @conn %>
<%= 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)