diff --git a/CHANGELOG.md b/CHANGELOG.md index 295875742f..5f80c101ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x" - [#1813](https://github.com/poanetwork/blockscout/pull/1813) - add total blocks counter to the main page - [#1806](https://github.com/poanetwork/blockscout/pull/1806) - verify contracts with a post request +- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir +- [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces ### Fixes @@ -13,7 +15,9 @@ - [#1840](https://github.com/poanetwork/blockscout/pull/1840) - Handle case when total supply is nil - [#1838](https://github.com/poanetwork/blockscout/pull/1838) - Block counter calculates only consensus blocks - [#1849](https://github.com/poanetwork/blockscout/pull/1849) - Improve chains menu +- [#1869](https://github.com/poanetwork/blockscout/pull/1869) - Fix output and gas extraction in JS tracer for Geth - [#1868](https://github.com/poanetwork/blockscout/pull/1868) - fix: logs list endpoint performance +- [#1822](https://github.com/poanetwork/blockscout/pull/1822) - Fix style breaks in decompiled contract code view ### Chore @@ -31,6 +35,7 @@ - [#1777](https://github.com/poanetwork/blockscout/pull/1777) - show ERC-20 token transfer info on transaction page - [#1770](https://github.com/poanetwork/blockscout/pull/1770) - set a websocket keepalive from config - [#1789](https://github.com/poanetwork/blockscout/pull/1789) - add ERC-721 info to transaction overview page +- [#1801](https://github.com/poanetwork/blockscout/pull/1801) - Staking pools fetching ### Fixes diff --git a/apps/block_scout_web/assets/css/_code.scss b/apps/block_scout_web/assets/css/_code.scss index f058b69676..b119bcf6d2 100644 --- a/apps/block_scout_web/assets/css/_code.scss +++ b/apps/block_scout_web/assets/css/_code.scss @@ -14,7 +14,7 @@ pre { .pre-decompiled code::before { content: counter(line); display: inline-block; - width: flex; + width: 3em; border-right: 1px solid #ddd; padding: 0 .5em; margin-right: .5em; diff --git a/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js b/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js new file mode 100644 index 0000000000..2ffb1efa6f --- /dev/null +++ b/apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js @@ -0,0 +1,7 @@ +import $ from 'jquery' +import hljs from 'highlight.js' + +// only activate highlighting on pages with this selector +if ($('[data-activate-highlight]').length > 0) { + hljs.initHighlightingOnLoad() +} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex new file mode 100644 index 0000000000..a73290faab --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex @@ -0,0 +1,55 @@ +defmodule BlockScoutWeb.TransactionRawTraceController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.TransactionView + alias Explorer.{Chain, Market} + alias Explorer.ExchangeRates.Token + + def index(conn, %{"transaction_id" => hash_string}) do + with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string), + {:ok, transaction} <- + Chain.hash_to_transaction( + hash, + necessity_by_association: %{ + :block => :optional, + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + [to_address: :smart_contract] => :optional, + :token_transfers => :optional + } + ) do + options = [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional + } + ] + + internal_transactions = Chain.transaction_to_internal_transactions(transaction, options) + + render( + conn, + "index.html", + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + internal_transactions: internal_transactions, + block_height: Chain.block_height(), + show_token_transfers: Chain.transaction_has_token_transfers?(hash), + transaction: transaction + ) + else + :error -> + conn + |> put_status(422) + |> put_view(TransactionView) + |> render("invalid.html", transaction_hash: hash_string) + + {:error, :not_found} -> + conn + |> put_status(404) + |> put_view(TransactionView) + |> render("not_found.html", transaction_hash: hash_string) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index a6e5a4b968..c0e975793f 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -99,6 +99,13 @@ defmodule BlockScoutWeb.Router do as: :internal_transaction ) + resources( + "/raw_trace", + TransactionRawTraceController, + only: [:index], + as: :raw_trace + ) + resources("/logs", TransactionLogController, only: [:index], as: :log) resources("/token_transfers", TransactionTokenTransferController, diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex index fb60b05e19..83bdb3372f 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex @@ -20,4 +20,9 @@ "data-test": "transaction_logs_link" ) %> + <%= link( + gettext("Raw Trace"), + class: "nav-link #{tab_status("raw_trace", @conn.request_path)}", + to: transaction_raw_trace_path(@conn, :index, @transaction) + ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex new file mode 100644 index 0000000000..85c3d6675f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex new file mode 100644 index 0000000000..2695dea13b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex @@ -0,0 +1,18 @@ +
+ <%= render BlockScoutWeb.TransactionView, "overview.html", assigns %> + +
+
+ <%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %> +
+ +
+

<%= gettext "Raw Trace" %>

+ <%= if Enum.count(@internal_transactions) > 0 do %> +
<%= for {line, number} <- raw_traces_with_lines(@internal_transactions) do %>
<%= line %>
<% end %>
+ <% else %> + No trace entries found. + <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex index 148d82b818..1418255594 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex @@ -18,13 +18,32 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do } def highlight_decompiled_code(code) do - @colors - |> Enum.reduce(code, fn {symbol, rgb}, acc -> - String.replace(acc, symbol, "") + {_, result} = + @colors + |> Enum.reduce(code, fn {symbol, rgb}, acc -> + String.replace(acc, symbol, "") + end) + |> String.replace("\e[1m", "") + |> String.replace("»", "»") + |> String.replace("\e[0m", "") + |> String.split(~r/\|\<\/span\>/, include_captures: true, trim: true) + |> Enum.reduce({"", []}, fn part, {style, acc} -> + new_style = + cond do + String.contains?(part, " part + part == "" -> "" + true -> style + end + + new_part = new_part(part, new_style) + + {new_style, [new_part | acc]} + end) + + result + |> Enum.reduce("", fn part, acc -> + part <> acc end) - |> String.replace("\e[1m", "") - |> String.replace("»", "»") - |> String.replace("\e[0m", "") |> add_line_numbers() end @@ -41,4 +60,34 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do acc <> "#{line}\n" end) end + + defp new_part(part, new_style) do + cond do + part == "" -> + "" + + part == "" -> + "" + + part == new_style -> + "" + + new_style == "" -> + part + + true -> + result = + part + |> String.split("\n") + |> Enum.reduce("", fn p, a -> + a <> new_style <> p <> "\n" + end) + + if String.ends_with?(part, "\n") do + result + else + String.slice(result, 0..-2) + end + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex new file mode 100644 index 0000000000..9d4a438582 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.TransactionRawTraceView do + use BlockScoutWeb, :view + @dialyzer :no_match + + alias Explorer.Chain.InternalTransaction + + def render("scripts.html", %{conn: conn}) do + render_scripts(conn, "raw_trace/code_highlighting.js") + end + + def raw_traces_with_lines(internal_transactions) do + internal_transactions + |> InternalTransaction.internal_transactions_to_raw() + |> Jason.encode!(pretty: true) + |> String.split("\n") + |> Enum.with_index(1) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 8fa133550f..418a7161de 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.TransactionView do import BlockScoutWeb.Gettext import BlockScoutWeb.Tokens.Helpers - @tabs ["token_transfers", "internal_transactions", "logs"] + @tabs ["token_transfers", "internal_transactions", "logs", "raw_trace"] defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction] @@ -338,6 +338,7 @@ defmodule BlockScoutWeb.TransactionView do defp tab_name(["token_transfers"]), do: gettext("Token Transfers") defp tab_name(["internal_transactions"]), do: gettext("Internal Transactions") defp tab_name(["logs"]), do: gettext("Logs") + defp tab_name(["raw_trace"]), do: gettext("Raw Trace") defp decode_params(params, types) do params diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index eb4adbee6d..6f198f8793 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -1732,3 +1732,10 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:225 msgid "Gas" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:10 +#: lib/block_scout_web/views/transaction_view.ex:341 +msgid "Raw Trace" +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 9d15643bdc..64201bee65 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 @@ -1732,3 +1732,10 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:225 msgid "Gas" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:10 +#: lib/block_scout_web/views/transaction_view.ex:341 +msgid "Raw Trace" +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs index acdf441750..f41a764c6e 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do configuration = Application.get_env(:explorer, Explorer.ExchangeRates) Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) + Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true) ExchangeRates.init([]) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs index 7c47ed622e..9a7c26e9ee 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs @@ -107,6 +107,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do configuration = Application.get_env(:explorer, Explorer.ExchangeRates) Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) + Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true) ExchangeRates.init([]) diff --git a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs index 9e12503c3a..c3ff123584 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs @@ -56,7 +56,21 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do result = AddressDecompiledContractView.highlight_decompiled_code(code) assert result == - " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: \n require (calldata.size - 4) >= 64\n return allowance[sha3(((320 - 1) and (320 - 1) and _owner), 1), ((320 - 1) and _spender and (320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: \n if (calldata.size - 4) < 32:\n revert\n else:\n if not (320 - 1) or not cd[4]:\n revert\n else:\n mem[0] = (320 - 1) and (320 - 1) and cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n return bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n" + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: 64\n return allowance[sha3(((320 - 1) and (320 - 1) and _owner), 1), ((320 - 1) and _spender and (320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: not cd[4]:\n revert\n else:\n mem[0]cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n return bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n" + end + + test "adds style span to every line" do + code = """ + # + # eveem.org 6 Feb 2019 + # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875 + # + # Let's make the world open source + #  + """ + + assert AddressDecompiledContractView.highlight_decompiled_code(code) == + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n\n" end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index b4a4e7c922..ab725c9c73 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -3,9 +3,10 @@ defmodule EthereumJSONRPC.Geth do Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth). """ - import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] - alias EthereumJSONRPC.Geth.Calls + alias EthereumJSONRPC.{FetchedBalance, FetchedCode} + alias EthereumJSONRPC.Geth.{Calls, Tracer} @behaviour EthereumJSONRPC.Variant @@ -28,7 +29,11 @@ defmodule EthereumJSONRPC.Geth do id_to_params |> debug_trace_transaction_requests() |> json_rpc(json_rpc_named_arguments) do - debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + debug_trace_transaction_responses_to_internal_transactions_params( + responses, + id_to_params, + json_rpc_named_arguments + ) end end @@ -62,13 +67,88 @@ defmodule EthereumJSONRPC.Geth do request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]}) end - defp debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + defp debug_trace_transaction_responses_to_internal_transactions_params( + [%{result: %{"structLogs" => _}} | _] = responses, + id_to_params, + json_rpc_named_arguments + ) + when is_map(id_to_params) do + with {:ok, receipts} <- + id_to_params + |> Enum.map(fn {id, %{hash_data: hash_data}} -> + request(%{id: id, method: "eth_getTransactionReceipt", params: [hash_data]}) + end) + |> json_rpc(json_rpc_named_arguments), + {:ok, txs} <- + id_to_params + |> Enum.map(fn {id, %{hash_data: hash_data}} -> + request(%{id: id, method: "eth_getTransactionByHash", params: [hash_data]}) + end) + |> json_rpc(json_rpc_named_arguments) do + receipts_map = Enum.into(receipts, %{}, fn %{id: id, result: receipt} -> {id, receipt} end) + txs_map = Enum.into(txs, %{}, fn %{id: id, result: tx} -> {id, tx} end) + + responses + |> Enum.map(fn %{id: id, result: %{"structLogs" => _} = result} -> + debug_trace_transaction_response_to_internal_transactions_params( + %{id: id, result: Tracer.replay(result, Map.fetch!(receipts_map, id), Map.fetch!(txs_map, id))}, + id_to_params + ) + end) + |> reduce_internal_transactions_params() + |> fetch_missing_data(json_rpc_named_arguments) + end + end + + defp debug_trace_transaction_responses_to_internal_transactions_params( + responses, + id_to_params, + _json_rpc_named_arguments + ) when is_list(responses) and is_map(id_to_params) do responses |> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params)) |> reduce_internal_transactions_params() end + defp fetch_missing_data({:ok, transactions}, json_rpc_named_arguments) when is_list(transactions) do + id_to_params = id_to_params(transactions) + + with {:ok, responses} <- + id_to_params + |> Enum.map(fn + {id, %{created_contract_address_hash: address, block_number: block_number}} -> + FetchedCode.request(%{id: id, block_quantity: integer_to_quantity(block_number), address: address}) + + {id, %{type: "selfdestruct", from: hash_data, block_number: block_number}} -> + FetchedBalance.request(%{id: id, block_quantity: integer_to_quantity(block_number), hash_data: hash_data}) + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + |> json_rpc(json_rpc_named_arguments) do + results = Enum.into(responses, %{}, fn %{id: id, result: result} -> {id, result} end) + + transactions = + id_to_params + |> Enum.map(fn + {id, %{created_contract_address_hash: _} = transaction} -> + %{transaction | created_contract_code: Map.fetch!(results, id)} + + {id, %{type: "selfdestruct"} = transaction} -> + %{transaction | value: Map.fetch!(results, id)} + + {_, transaction} -> + transaction + end) + + {:ok, transactions} + end + end + + defp fetch_missing_data(result, _json_rpc_named_arguments), do: result + defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, result: calls}, id_to_params) when is_map(id_to_params) do %{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} = diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex index 88a32c704f..434cc7b34d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex @@ -328,9 +328,8 @@ defmodule EthereumJSONRPC.Geth.Call do "from" => from_address_hash, "to" => to_address_hash, "gas" => gas, - "gasUsed" => gas_used, "input" => input, - "output" => output, + "error" => error, "value" => value }) when call_type in ~w(call callcode delegatecall) do @@ -345,9 +344,8 @@ defmodule EthereumJSONRPC.Geth.Call do from_address_hash: from_address_hash, to_address_hash: to_address_hash, gas: gas, - gas_used: gas_used, input: input, - output: output, + error: error, value: value } end @@ -363,8 +361,9 @@ defmodule EthereumJSONRPC.Geth.Call do "from" => from_address_hash, "to" => to_address_hash, "gas" => gas, + "gasUsed" => gas_used, "input" => input, - "error" => error, + "output" => output, "value" => value }) when call_type in ~w(call callcode delegatecall) do @@ -379,8 +378,9 @@ defmodule EthereumJSONRPC.Geth.Call do from_address_hash: from_address_hash, to_address_hash: to_address_hash, gas: gas, + gas_used: gas_used, input: input, - error: error, + output: output, value: value } end @@ -425,13 +425,11 @@ defmodule EthereumJSONRPC.Geth.Call do "transactionHash" => transaction_hash, "index" => index, "traceAddress" => trace_address, - "type" => "create", + "type" => "create" = type, "from" => from_address_hash, - "createdContractAddressHash" => created_contract_address_hash, + "error" => error, "gas" => gas, - "gasUsed" => gas_used, "init" => init, - "createdContractCode" => created_contract_code, "value" => value }) do %{ @@ -440,13 +438,11 @@ defmodule EthereumJSONRPC.Geth.Call do transaction_hash: transaction_hash, index: index, trace_address: trace_address, - type: "create", + type: type, from_address_hash: from_address_hash, gas: gas, - gas_used: gas_used, - created_contract_address_hash: created_contract_address_hash, + error: error, init: init, - created_contract_code: created_contract_code, value: value } end @@ -457,11 +453,13 @@ defmodule EthereumJSONRPC.Geth.Call do "transactionHash" => transaction_hash, "index" => index, "traceAddress" => trace_address, - "type" => "create" = type, + "type" => "create", "from" => from_address_hash, - "error" => error, + "createdContractAddressHash" => created_contract_address_hash, "gas" => gas, + "gasUsed" => gas_used, "init" => init, + "createdContractCode" => created_contract_code, "value" => value }) do %{ @@ -470,11 +468,13 @@ defmodule EthereumJSONRPC.Geth.Call do transaction_hash: transaction_hash, index: index, trace_address: trace_address, - type: type, + type: "create", from_address_hash: from_address_hash, gas: gas, - error: error, + gas_used: gas_used, + created_contract_address_hash: created_contract_address_hash, init: init, + created_contract_code: created_contract_code, value: value } end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex new file mode 100644 index 0000000000..6545250b4a --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex @@ -0,0 +1,282 @@ +defmodule EthereumJSONRPC.Geth.Tracer do + @moduledoc """ + Elixir implementation of a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`) + for variants that don't support specifying tracer in [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction) calls. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] + + def replay(%{"structLogs" => logs} = result, receipt, tx) when is_list(logs) do + %{"contractAddress" => contract_address} = receipt + %{"from" => from, "to" => to, "value" => value, "input" => input} = tx + + top = + to + |> if do + %{ + "type" => "call", + "callType" => "call", + "to" => to, + "input" => input, + "output" => Map.get(result, "return", "0x" <> Map.get(result, "returnValue", "")) + } + else + %{ + "type" => "create", + "init" => input, + "createdContractAddressHash" => contract_address, + "createdContractCode" => "0x" + } + end + |> Map.merge(%{ + "from" => from, + "traceAddress" => [], + "value" => value, + "gas" => 0, + "gasUsed" => 0 + }) + + ctx = %{ + depth: 1, + stack: [top], + trace_address: [0], + calls: [[]] + } + + logs + |> Enum.reduce(ctx, &step/2) + |> finalize() + end + + defp step(%{"error" => _}, %{stack: [%{"error" => _} | _]} = ctx), do: ctx + + defp step( + %{"error" => _} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) do + call = process_return(log, Map.put(call, "error", "error")) + + subsubcalls = + subsubcalls + |> Enum.reverse() + |> Enum.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + + %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + } + end + + defp step( + %{"depth" => log_depth} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) + when log_depth == stack_depth - 1 do + call = process_return(log, call) + + subsubcalls = + subsubcalls + |> Enum.reverse() + |> Enum.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + + step(log, %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + }) + end + + defp step(%{"gas" => log_gas, "gasCost" => log_gas_cost} = log, %{stack: [%{"gas" => call_gas} = call | stack]} = ctx) do + gas = max(call_gas, log_gas) + op(log, %{ctx | stack: [%{call | "gas" => gas, "gasUsed" => gas - log_gas - log_gas_cost} | stack]}) + end + + defp op(%{"op" => "CREATE"} = log, ctx), do: create_op(log, ctx) + defp op(%{"op" => "SELFDESTRUCT"} = log, ctx), do: self_destruct_op(log, ctx) + defp op(%{"op" => "CALL"} = log, ctx), do: call_op(log, "call", ctx) + defp op(%{"op" => "CALLCODE"} = log, ctx), do: call_op(log, "callcode", ctx) + defp op(%{"op" => "DELEGATECALL"} = log, ctx), do: call_op(log, "delegatecall", ctx) + defp op(%{"op" => "STATICCALL"} = log, ctx), do: call_op(log, "staticcall", ctx) + defp op(%{"op" => "REVERT"}, ctx), do: revert_op(ctx) + defp op(_, ctx), do: ctx + + defp process_return(%{"stack" => log_stack}, %{"type" => "create"} = call) do + [ret | _] = Enum.reverse(log_stack) + + case quantity_to_integer(ret) do + 0 -> Map.put(call, "error", call["error"] || "internal failure") + _ -> %{call | "createdContractAddressHash" => "0x" <> String.slice(ret, 24, 40)} + end + end + + defp process_return( + %{"stack" => log_stack, "memory" => log_memory}, + %{"outputOffset" => out_off, "outputLength" => out_len} = call + ) do + [ret | _] = Enum.reverse(log_stack) + + ret + |> quantity_to_integer() + |> case do + 0 -> + Map.put(call, "error", call["error"] || "internal failure") + + _ -> + output = + log_memory + |> IO.iodata_to_binary() + |> String.slice(out_off, out_len) + + %{call | "output" => "0x" <> output} + end + |> Map.drop(["outputOffset", "outputLength"]) + end + + defp create_op( + %{"stack" => log_stack, "memory" => log_memory}, + %{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx + ) do + [value, input_offset, input_length | _] = Enum.reverse(log_stack) + + init = + log_memory + |> IO.iodata_to_binary() + |> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) + + call = %{ + "type" => "create", + "from" => nil, + "traceAddress" => Enum.reverse(trace_address), + "init" => "0x" <> init, + "gas" => 0, + "gasUsed" => 0, + "value" => "0x" <> value, + "createdContractAddressHash" => nil, + "createdContractCode" => "0x" + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call | stack], + trace_address: [0 | trace_address], + calls: [[] | calls] + } + end + + defp self_destruct_op( + %{"stack" => log_stack, "gas" => log_gas, "gasCost" => log_gas_cost}, + %{trace_address: [trace_index | trace_address], calls: [subcalls | calls]} = ctx + ) do + [to | _] = Enum.reverse(log_stack) + + if quantity_to_integer(to) in 1..8 do + ctx + else + call = %{ + "type" => "selfdestruct", + "from" => nil, + "to" => "0x" <> String.slice(to, 24, 40), + "traceAddress" => Enum.reverse([trace_index | trace_address]), + "gas" => log_gas, + "gasUsed" => log_gas_cost, + "value" => "0x0" + } + + %{ctx | trace_address: [trace_index + 1 | trace_address], calls: [[call | subcalls] | calls]} + end + end + + defp call_op( + %{"stack" => log_stack, "memory" => log_memory}, + call_type, + %{ + depth: stack_depth, + stack: [%{"value" => parent_value} = parent | stack], + trace_address: trace_address, + calls: calls + } = ctx + ) do + [_, to | log_stack] = Enum.reverse(log_stack) + + {value, [input_offset, input_length, output_offset, output_length | _]} = + case call_type do + "delegatecall" -> + {parent_value, log_stack} + + "staticcall" -> + {"0x0", log_stack} + + _ -> + [value | rest] = log_stack + {"0x" <> value, rest} + end + + input = + log_memory + |> IO.iodata_to_binary() + |> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) + + call = %{ + "type" => "call", + "callType" => call_type, + "from" => nil, + "to" => "0x" <> String.slice(to, 24, 40), + "traceAddress" => Enum.reverse(trace_address), + "input" => "0x" <> input, + "output" => "0x", + "outputOffset" => quantity_to_integer("0x" <> output_offset) * 2, + "outputLength" => quantity_to_integer("0x" <> output_length) * 2, + "gas" => 0, + "gasUsed" => 0, + "value" => value + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call, parent | stack], + trace_address: [0 | trace_address], + calls: [[] | calls] + } + end + + defp revert_op(%{stack: [last | stack]} = ctx) do + %{ctx | stack: [Map.put(last, "error", "execution reverted") | stack]} + end + + defp finalize(%{stack: [top], calls: [calls]}) do + calls = + Enum.map(calls, fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => top["createdContractAddressHash"] || top["to"]} + end) + + [top | Enum.reverse(calls)] + |> List.flatten() + |> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call -> + %{call | "gas" => integer_to_quantity(gas), "gasUsed" => integer_to_quantity(gas_used)} + end) + end +end diff --git a/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js b/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js index 23dc279edc..15db7d8ff9 100644 --- a/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js +++ b/apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js @@ -73,6 +73,15 @@ topCall.calls.push(childCall); }, + pushGasToTopCall(log) { + const topCall = this.topCall(); + + if (topCall.gasBigInt === undefined) { + topCall.gasBigInt = log.getGas(); + } + topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost(); + }, + success(log, db) { const op = log.op.toString(); @@ -115,8 +124,6 @@ // Pop off the last call and get the execution results const call = this.callStack.pop(); - call.gasUsedBigInt = call.gasBigInt.subtract(log.getGas()); - const ret = log.stack.peek(0); if (!ret.equals(0)) { @@ -124,7 +131,7 @@ call.createdContractAddressHash = toHex(toAddress(ret.toString(16))); call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16)))); } else { - call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen)); + call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength)); } } else if (call.error === undefined) { call.error = 'internal failure'; @@ -135,6 +142,9 @@ this.pushChildCall(call); } + else { + this.pushGasToTopCall(log); + } }, createOp(log) { @@ -147,7 +157,6 @@ type: 'create', from: toHex(log.contract.getAddress()), init: toHex(log.memory.slice(inputOffset, inputEnd)), - gasBigInt: bigInt(log.getGas()), valueBigInt: bigInt(stackValue.toString(10)) }; this.callStack.push(call); @@ -160,7 +169,8 @@ type: 'selfdestruct', from: toHex(contractAddress), to: toHex(toAddress(log.stack.peek(0).toString(16))), - gasBigInt: bigInt(log.getGas()), + gasBigInt: log.getGas(), + gasUsedBigInt: log.getCost(), valueBigInt: db.getBalance(contractAddress) }); }, @@ -186,7 +196,6 @@ callType: op.toLowerCase(), from: toHex(log.contract.getAddress()), to: toHex(to), - gasBigInt: bigInt(log.getGas()), input: toHex(log.memory.slice(inputOffset, inputEnd)), outputOffset: log.stack.peek(4 + stackOffset).valueOf(), outputLength: log.stack.peek(5 + stackOffset).valueOf() @@ -220,7 +229,7 @@ result(ctx, db) { const result = this.ctxToResult(ctx, db); const filtered = this.filterNotUndefined(result); - const callSequence = this.sequence(filtered, [], filtered.valueBigInt, filtered.gasUsedBigInt, []).callSequence; + const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence; return this.encodeCallSequence(callSequence); }, @@ -339,7 +348,7 @@ }, // sequence converts the finalized calls from a call tree to a call sequence - sequence(call, callSequence, availableValueBigInt, availableGasBigInt, traceAddress) { + sequence(call, callSequence, availableValueBigInt, traceAddress) { const subcalls = call.calls; delete call.calls; @@ -347,38 +356,24 @@ if (call.type === 'call' && call.callType === 'delegatecall') { call.valueBigInt = availableValueBigInt; - } else if (call.type === 'selfdestruct') { - call.gasUsedBigInt = availableGasBigInt } var newCallSequence = callSequence.concat([call]); if (subcalls !== undefined) { - var nestedAvailableValueBigInt = availableValueBigInt; - var nestedAvailableGasBigInt = availableGasBigInt; - for (var i = 0; i < subcalls.length; i++) { const nestedSequenced = this.sequence( subcalls[i], newCallSequence, - nestedAvailableValueBigInt, - availableGasBigInt, + call.valueBigInt, traceAddress.concat([i]) ); newCallSequence = nestedSequenced.callSequence; - nestedAvailableValueBigInt = nestedSequenced.availableValueBigInt; - nestedAvailableGasBigInt = nestedSequenced.availableGasBigInt; } } - const newAvailableValueBigInt = availableValueBigInt.subtract(call.valueBigInt); - - const newAvailableGasUsedBigInt = availableGasBigInt.subtract(call.gasUsedBigInt); - return { - callSequence: newCallSequence, - availableValueBigInt: newAvailableValueBigInt, - availableGasBigInt: newAvailableGasUsedBigInt + callSequence: newCallSequence }; }, @@ -410,7 +405,7 @@ delete call.gasBigInt; if (gasBigInt === undefined) { - throw "gasBigInt undefined in " + JSON.stringify(call); + gasBigInt = bigInt.zero; } call.gas = '0x' + gasBigInt.toString(16); @@ -421,7 +416,7 @@ delete call.gasUsedBigInt; if (gasUsedBigInt === undefined) { - throw "gasUsedBigInt undefined in " + JSON.stringify(call); + gasUsedBigInt = bigInt.zero; } call.gasUsed = '0x' + gasUsedBigInt.toString(16); diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index a351248a4f..ace69884ea 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -54,6 +54,10 @@ else config :explorer, Explorer.Validator.MetadataProcessor, enabled: false end +config :explorer, Explorer.Staking.PoolsReader, + validators_contract_address: System.get_env("POS_VALIDATORS_CONTRACT"), + staking_contract_address: System.get_env("POS_STAKING_CONTRACT") + if System.get_env("SUPPLY_MODULE") == "TokenBridge" do config :explorer, supply: Explorer.Chain.Supply.TokenBridge end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 080b880211..47b30cfa2b 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -56,9 +56,7 @@ defmodule Explorer.Application do end defp should_start?(process) do - :explorer - |> Application.fetch_env!(process) - |> Keyword.fetch!(:enabled) + Application.get_env(:explorer, process, [])[:enabled] == true end defp configure(process) do diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex new file mode 100644 index 0000000000..aaf5d7242e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -0,0 +1,88 @@ +defmodule Explorer.Chain.Import.Runner.StakingPools do + @moduledoc """ + Bulk imports staking pools to Address.Name tabe. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.{Address, Import} + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Address.Name.t()] + + @impl Import.Runner + def ecto_schema_module, do: Address.Name + + @impl Import.Runner + def option_key, do: :staking_pools + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + multi + |> Multi.run(:insert_staking_pools, fn repo, _ -> + insert(repo, changes_list, insert_options) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [Address.Name.t()]} + | {:error, [Changeset.t()]} + defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + {:ok, _} = + Import.insert_changes_list( + repo, + changes_list, + conflict_target: {:unsafe_fragment, "(address_hash) where \"primary\" = true"}, + on_conflict: on_conflict, + for: Address.Name, + returning: [:address_hash], + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + from( + name in Address.Name, + update: [ + set: [ + name: fragment("EXCLUDED.name"), + metadata: fragment("EXCLUDED.metadata"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", name.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", name.updated_at) + ] + ] + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index f652309c4d..4f1d2d6fe5 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -86,7 +86,8 @@ defmodule Explorer.Chain.Import.Runner.Transactions do conflict_target: :hash, on_conflict: on_conflict, for: Transaction, - returning: ~w(block_number index hash internal_transactions_indexed_at block_hash nonce from_address_hash)a, + returning: + ~w(block_number index hash internal_transactions_indexed_at block_hash nonce from_address_hash created_contract_address_hash)a, timeout: timeout, timestamps: timestamps ) diff --git a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex index f6aac010b0..0e42bdc1f9 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex @@ -24,7 +24,8 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do Runner.Tokens, Runner.TokenTransfers, Runner.Address.CurrentTokenBalances, - Runner.Address.TokenBalances + Runner.Address.TokenBalances, + Runner.StakingPools ] @impl Stage diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 6a8fa7b6bf..60a0f3ed08 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -4,7 +4,7 @@ defmodule Explorer.Chain.InternalTransaction do use Explorer.Schema alias Explorer.Chain.{Address, Data, Gas, Hash, Transaction, Wei} - alias Explorer.Chain.InternalTransaction.{CallType, Type} + alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type} @typedoc """ * `block_number` - the `t:Explorer.Chain.Block.t/0` `number` that the `transaction` is collated into. @@ -497,4 +497,121 @@ defmodule Explorer.Chain.InternalTransaction do def where_block_number_is_not_null(query) do where(query, [t], not is_nil(t.block_number)) end + + def internal_transactions_to_raw(internal_transactions) when is_list(internal_transactions) do + internal_transactions + |> Enum.map(&internal_transaction_to_raw/1) + |> add_subtraces() + end + + defp internal_transaction_to_raw(%{type: :call} = transaction) do + %{ + call_type: call_type, + to_address_hash: to_address_hash, + from_address_hash: from_address_hash, + input: input, + gas: gas, + value: value, + trace_address: trace_address + } = transaction + + action = %{ + "callType" => call_type, + "to" => to_address_hash, + "from" => from_address_hash, + "input" => input, + "gas" => gas, + "value" => value + } + + %{ + "type" => "call", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + |> put_raw_call_error_or_result(transaction) + end + + defp internal_transaction_to_raw(%{type: :create} = transaction) do + %{ + from_address_hash: from_address_hash, + gas: gas, + init: init, + trace_address: trace_address, + value: value + } = transaction + + action = %{"from" => from_address_hash, "gas" => gas, "init" => init, "value" => value} + + %{ + "type" => "create", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + |> put_raw_create_error_or_result(transaction) + end + + defp internal_transaction_to_raw(%{type: :selfdestruct} = transaction) do + %{ + to_address_hash: to_address_hash, + from_address_hash: from_address_hash, + trace_address: trace_address, + value: value + } = transaction + + action = %{ + "address" => from_address_hash, + "balance" => value, + "refundAddress" => to_address_hash + } + + %{ + "type" => "suicide", + "action" => Action.to_raw(action), + "traceAddress" => trace_address + } + end + + defp add_subtraces(traces) do + Enum.map(traces, fn trace -> + Map.put(trace, "subtraces", count_subtraces(trace, traces)) + end) + end + + defp count_subtraces(%{"traceAddress" => trace_address}, traces) do + Enum.count(traces, fn %{"traceAddress" => trace_address_candidate} -> + direct_descendant?(trace_address, trace_address_candidate) + end) + end + + defp direct_descendant?([], [_]), do: true + + defp direct_descendant?([elem | remaining_left], [elem | remaining_right]), + do: direct_descendant?(remaining_left, remaining_right) + + defp direct_descendant?(_, _), do: false + + defp put_raw_call_error_or_result(raw, %{error: error}) when not is_nil(error) do + Map.put(raw, "error", error) + end + + defp put_raw_call_error_or_result(raw, %{gas_used: gas_used, output: output}) do + Map.put(raw, "result", Result.to_raw(%{"gasUsed" => gas_used, "output" => output})) + end + + defp put_raw_create_error_or_result(raw, %{error: error}) when not is_nil(error) do + Map.put(raw, "error", error) + end + + defp put_raw_create_error_or_result(raw, %{ + created_contract_code: code, + created_contract_address_hash: created_contract_address_hash, + gas_used: gas_used + }) do + Map.put( + raw, + "result", + Result.to_raw(%{"gasUsed" => gas_used, "code" => code, "address" => created_contract_address_hash}) + ) + end end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/action.ex b/apps/explorer/lib/explorer/chain/internal_transaction/action.ex new file mode 100644 index 0000000000..663e306515 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/action.ex @@ -0,0 +1,42 @@ +defmodule Explorer.Chain.InternalTransaction.Action do + @moduledoc """ + The action that was performed in a `t:EthereumJSONRPC.Parity.Trace.t/0` + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias Explorer.Chain.{Data, Hash, Wei} + + def to_raw(action) when is_map(action) do + Enum.into(action, %{}, &entry_to_raw/1) + end + + defp entry_to_raw({key, %Data{} = data}) when key in ~w(init input) do + {key, Data.to_string(data)} + end + + defp entry_to_raw({key, %Hash{} = address}) when key in ~w(address from refundAddress to) do + {key, to_string(address)} + end + + defp entry_to_raw({"callType", type}) do + {"callType", Atom.to_string(type)} + end + + defp entry_to_raw({"gas" = key, %Decimal{} = decimal}) do + value = + decimal + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(value)} + end + + defp entry_to_raw({key, %Wei{value: value}}) when key in ~w(balance value) do + rounded = + value + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(rounded)} + end +end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex new file mode 100644 index 0000000000..5b4e3102fc --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex @@ -0,0 +1,32 @@ +defmodule Explorer.Chain.InternalTransaction.Result do + @moduledoc """ + The result of performing the `t:EthereumJSONRPC.Parity.Action.t/0` in a `t:EthereumJSONRPC.Parity.Trace.t/0`. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + alias Explorer.Chain.{Data, Hash} + + def to_raw(result) when is_map(result) do + Enum.into(result, %{}, &entry_to_raw/1) + end + + defp entry_to_raw({"output" = key, %Data{} = data}) do + {key, Data.to_string(data)} + end + + defp entry_to_raw({"address" = key, %Hash{} = hash}) do + {key, to_string(hash)} + end + + defp entry_to_raw({"code", _} = entry), do: entry + + defp entry_to_raw({key, decimal}) when key in ~w(gasUsed) do + integer = + decimal + |> Decimal.round() + |> Decimal.to_integer() + + {key, integer_to_quantity(integer)} + end +end diff --git a/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex b/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex index 514751d8d6..f0ed6f0f54 100644 --- a/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex +++ b/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex @@ -90,7 +90,7 @@ defmodule Explorer.ExchangeRates do """ @spec lookup(String.t()) :: Token.t() | nil def lookup(symbol) do - if store() == :ets do + if store() == :ets && enabled?() do case :ets.lookup(table_name(), symbol) do [tuple | _] when is_tuple(tuple) -> Token.from_tuple(tuple) _ -> nil @@ -133,4 +133,8 @@ defmodule Explorer.ExchangeRates do defp store do config(:store) || :ets end + + defp enabled? do + Application.get_env(:explorer, __MODULE__, [])[:enabled] == true + end end diff --git a/apps/explorer/lib/explorer/known_tokens/known_tokens.ex b/apps/explorer/lib/explorer/known_tokens/known_tokens.ex index baa09784ba..aff9dea74b 100644 --- a/apps/explorer/lib/explorer/known_tokens/known_tokens.ex +++ b/apps/explorer/lib/explorer/known_tokens/known_tokens.ex @@ -81,7 +81,11 @@ defmodule Explorer.KnownTokens do """ @spec list :: [{String.t(), Hash.Address.t()}] def list do - list_from_store(store()) + if enabled?() do + list_from_store(store()) + else + [] + end end @doc """ @@ -89,7 +93,7 @@ defmodule Explorer.KnownTokens do """ @spec lookup(String.t()) :: {:ok, Hash.Address.t()} | :error | nil def lookup(symbol) do - if store() == :ets do + if store() == :ets && enabled?() do case :ets.lookup(table_name(), symbol) do [{_symbol, address} | _] -> Hash.Address.cast(address) _ -> nil @@ -128,4 +132,8 @@ defmodule Explorer.KnownTokens do defp store do config(:store) || :ets end + + defp enabled? do + Application.get_env(:explorer, __MODULE__, [])[:enabled] == true + end end diff --git a/apps/explorer/lib/explorer/staking/pools_reader.ex b/apps/explorer/lib/explorer/staking/pools_reader.ex new file mode 100644 index 0000000000..de03ff10b5 --- /dev/null +++ b/apps/explorer/lib/explorer/staking/pools_reader.ex @@ -0,0 +1,121 @@ +defmodule Explorer.Staking.PoolsReader do + @moduledoc """ + Reads staking pools using Smart Contract functions from the blockchain. + """ + alias Explorer.SmartContract.Reader + + @spec get_pools() :: [String.t()] + def get_pools do + get_active_pools() ++ get_inactive_pools() + end + + @spec get_active_pools() :: [String.t()] + def get_active_pools do + {:ok, [active_pools]} = call_staking_method("getPools", []) + active_pools + end + + @spec get_inactive_pools() :: [String.t()] + def get_inactive_pools do + {:ok, [inactive_pools]} = call_staking_method("getPoolsInactive", []) + inactive_pools + end + + @spec pool_data(String.t()) :: {:ok, map()} | :error + def pool_data(staking_address) do + with {:ok, [mining_address]} <- call_validators_method("miningByStakingAddress", [staking_address]), + data = fetch_data(staking_address, mining_address), + {:ok, [is_active]} <- data["isPoolActive"], + {:ok, [delegator_addresses]} <- data["poolDelegators"], + delegators_count = Enum.count(delegator_addresses), + {:ok, [staked_amount]} <- data["stakeAmountTotalMinusOrderedWithdraw"], + {:ok, [is_validator]} <- data["isValidator"], + {:ok, [was_validator_count]} <- data["validatorCounter"], + {:ok, [is_banned]} <- data["isValidatorBanned"], + {:ok, [banned_until]} <- data["bannedUntil"], + {:ok, [was_banned_count]} <- data["banCounter"] do + { + :ok, + %{ + staking_address: staking_address, + mining_address: mining_address, + is_active: is_active, + delegators_count: delegators_count, + staked_amount: staked_amount, + is_validator: is_validator, + was_validator_count: was_validator_count, + is_banned: is_banned, + banned_until: banned_until, + was_banned_count: was_banned_count + } + } + else + _ -> + :error + end + end + + defp call_staking_method(method, params) do + %{^method => resp} = + Reader.query_contract(config(:staking_contract_address), abi("staking.json"), %{ + method => params + }) + + resp + end + + defp call_validators_method(method, params) do + %{^method => resp} = + Reader.query_contract(config(:validators_contract_address), abi("validators.json"), %{ + method => params + }) + + resp + end + + defp fetch_data(staking_address, mining_address) do + contract_abi = abi("staking.json") ++ abi("validators.json") + + methods = [ + {:staking, "isPoolActive", staking_address}, + {:staking, "poolDelegators", staking_address}, + {:staking, "stakeAmountTotalMinusOrderedWithdraw", staking_address}, + {:validators, "isValidator", mining_address}, + {:validators, "validatorCounter", mining_address}, + {:validators, "isValidatorBanned", mining_address}, + {:validators, "bannedUntil", mining_address}, + {:validators, "banCounter", mining_address} + ] + + methods + |> Enum.map(&format_request/1) + |> Reader.query_contracts(contract_abi) + |> Enum.zip(methods) + |> Enum.into(%{}, fn {response, {_, function_name, _}} -> + {function_name, response} + end) + end + + defp format_request({contract_name, function_name, param}) do + %{ + contract_address: contract(contract_name), + function_name: function_name, + args: [param] + } + end + + defp contract(:staking), do: config(:staking_contract_address) + defp contract(:validators), do: config(:validators_contract_address) + + defp config(key) do + Application.get_env(:explorer, __MODULE__, [])[key] + end + + # sobelow_skip ["Traversal"] + defp abi(file_name) do + :explorer + |> Application.app_dir("priv/contracts_abi/pos/#{file_name}") + |> File.read!() + |> Jason.decode!() + end +end diff --git a/apps/explorer/lib/explorer/validator/metadata_retriever.ex b/apps/explorer/lib/explorer/validator/metadata_retriever.ex index 682b1e906e..4d262fca61 100644 --- a/apps/explorer/lib/explorer/validator/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/validator/metadata_retriever.ex @@ -69,7 +69,7 @@ defmodule Explorer.Validator.MetadataRetriever do # sobelow_skip ["Traversal"] defp contract_abi(file_name) do :explorer - |> Application.app_dir("priv/validator_contracts_abi/#{file_name}") + |> Application.app_dir("priv/contracts_abi/poa/#{file_name}") |> File.read!() |> Jason.decode!() end diff --git a/apps/explorer/priv/validator_contracts_abi/metadata.json b/apps/explorer/priv/contracts_abi/poa/metadata.json similarity index 100% rename from apps/explorer/priv/validator_contracts_abi/metadata.json rename to apps/explorer/priv/contracts_abi/poa/metadata.json diff --git a/apps/explorer/priv/validator_contracts_abi/validators.json b/apps/explorer/priv/contracts_abi/poa/validators.json similarity index 100% rename from apps/explorer/priv/validator_contracts_abi/validators.json rename to apps/explorer/priv/contracts_abi/poa/validators.json diff --git a/apps/explorer/priv/contracts_abi/pos/staking.json b/apps/explorer/priv/contracts_abi/pos/staking.json new file mode 100644 index 0000000000..7bcbcfb18c --- /dev/null +++ b/apps/explorer/priv/contracts_abi/pos/staking.json @@ -0,0 +1,925 @@ +[ + { + "constant": true, + "inputs": [], + "name": "STAKE_UNIT", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_DELEGATORS_PER_POOL", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_CANDIDATES", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "Claimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "toPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "toPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "StakeMoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "int256" + } + ], + "name": "WithdrawalOrdered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { + "name": "_unremovableStakingAddress", + "type": "address" + } + ], + "name": "clearUnremovableValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "incrementStakingEpoch", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "removePool", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "removePool", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_fromPoolStakingAddress", + "type": "address" + }, + { + "name": "_toPoolStakingAddress", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "moveStake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_toPoolStakingAddress", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_fromPoolStakingAddress", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_amount", + "type": "int256" + } + ], + "name": "orderWithdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "claimOrderedWithdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_erc20TokenContract", + "type": "address" + } + ], + "name": "setErc20TokenContract", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_minStake", + "type": "uint256" + } + ], + "name": "setCandidateMinStake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_minStake", + "type": "uint256" + } + ], + "name": "setDelegatorMinStake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPools", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPoolsInactive", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPoolsLikelihood", + "outputs": [ + { + "name": "likelihoods", + "type": "int256[]" + }, + { + "name": "sum", + "type": "int256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPoolsToBeElected", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPoolsToBeRemoved", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "areStakeAndWithdrawAllowed", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "erc20TokenContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCandidateMinStake", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getDelegatorMinStake", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "isPoolActive", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "maxWithdrawAllowed", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "maxWithdrawOrderAllowed", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "bytes" + } + ], + "name": "onTokenTransfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "orderedWithdrawAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "orderedWithdrawAmountTotal", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "orderWithdrawEpoch", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "stakeAmountTotal", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "poolDelegators", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_delegator", + "type": "address" + } + ], + "name": "poolDelegatorIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_delegator", + "type": "address" + } + ], + "name": "poolDelegatorInactiveIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "poolIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "poolInactiveIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "poolToBeElectedIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "poolToBeRemovedIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "stakeAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "stakeAmountByCurrentEpoch", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "stakeAmountMinusOrderedWithdraw", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "stakeAmountTotalMinusOrderedWithdraw", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "stakingEpoch", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "validatorSetContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/apps/explorer/priv/contracts_abi/pos/validators.json b/apps/explorer/priv/contracts_abi/pos/validators.json new file mode 100644 index 0000000000..0f0fd038c6 --- /dev/null +++ b/apps/explorer/priv/contracts_abi/pos/validators.json @@ -0,0 +1,492 @@ +[ + { + "constant": false, + "inputs": [], + "name": "newValidatorSet", + "outputs": [ + { + "name": "", + "type": "bool" + }, + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_VALIDATORS", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "indexed": true, + "name": "parentHash", + "type": "bytes32" + }, + { + "indexed": false, + "name": "newSet", + "type": "address[]" + } + ], + "name": "InitiateChange", + "type": "event", + "anonymous": false + }, + { + "inputs": [], + "name": "clearUnremovableValidator", + "type": "function", + "constant": false, + "outputs": [], + "payable": false, + "stateMutability": "nonpayable" + }, + { + "inputs": [], + "name": "emitInitiateChange", + "type": "function", + "constant": false, + "outputs": [], + "payable": false, + "stateMutability": "nonpayable" + }, + { + "inputs": [], + "name": "finalizeChange", + "type": "function", + "constant": false, + "outputs": [], + "payable": false, + "stateMutability": "nonpayable" + }, + { + "inputs": [ + { + "name": "_blockRewardContract", + "type": "address" + }, + { + "name": "_randomContract", + "type": "address" + }, + { + "name": "_stakingContract", + "type": "address" + }, + { + "name": "_initialMiningAddresses", + "type": "address[]" + }, + { + "name": "_initialStakingAddresses", + "type": "address[]" + }, + { + "name": "_firstValidatorIsUnremovable", + "type": "bool" + } + ], + "name": "initialize", + "type": "function", + "constant": false, + "outputs": [], + "payable": false, + "stateMutability": "nonpayable" + }, + { + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + }, + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "setStakingAddress", + "type": "function", + "constant": false, + "outputs": [], + "payable": false, + "stateMutability": "nonpayable" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "banCounter", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "bannedUntil", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "blockRewardContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "changeRequestCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "emitInitiateChangeCallable", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPreviousValidators", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPendingValidators", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getQueueValidators", + "outputs": [ + { + "name": "", + "type": "address[]" + }, + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getValidators", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "initiateChangeAllowed", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "isReportValidatorValid", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "isValidator", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "isValidatorOnPreviousEpoch", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "isValidatorBanned", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "miningByStakingAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "randomContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "stakingByMiningAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "stakingContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "unremovableValidator", + "outputs": [ + { + "name": "stakingAddress", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "validatorCounter", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "validatorIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "validatorSetApplyBlock", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs new file mode 100644 index 0000000000..d5bc6ecfca --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs @@ -0,0 +1,94 @@ +defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do + use Explorer.DataCase + + alias Ecto.Multi + alias Explorer.Chain.Import.Runner.StakingPools + + describe "run/1" do + test "insert new pools list" do + pools = [ + %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>> + }, + metadata: %{ + banned_unitil: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + mining_address: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>> + }, + retries_count: 1, + staked_amount: 0, + was_banned_count: 0, + was_validator_count: 1 + }, + name: "anonymous", + primary: true + }, + %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<170, 148, 182, 135, 211, 249, 85, 42, 69, 59, 129, 178, 131, 76, 165, 55, 120, 152, 13, 192>> + }, + metadata: %{ + banned_unitil: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + mining_address: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<117, 223, 66, 56, 58, 254, 107, 245, 25, 74, 168, 250, 14, 155, 61, 95, 158, 134, 148, 65>> + }, + retries_count: 1, + staked_amount: 0, + was_banned_count: 0, + was_validator_count: 1 + }, + name: "anonymous", + primary: true + }, + %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<49, 44, 35, 14, 125, 109, 176, 82, 36, 246, 2, 8, 166, 86, 227, 84, 28, 92, 66, 186>> + }, + metadata: %{ + banned_unitil: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + mining_address: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<82, 45, 243, 150, 174, 112, 160, 88, 189, 105, 119, 132, 8, 99, 15, 219, 2, 51, 137, 178>> + }, + retries_count: 1, + staked_amount: 0, + was_banned_count: 0, + was_validator_count: 1 + }, + name: "anonymous", + primary: true + } + ] + + assert {:ok, %{insert_staking_pools: list}} = run_changes(pools) + assert Enum.count(list) == Enum.count(pools) + end + end + + defp run_changes(changes) do + Multi.new() + |> StakingPools.run(changes, %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + }) + |> Repo.transaction() + end +end diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index 2b92339515..54ace519bd 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,7 +1,10 @@ defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase - alias Explorer.Chain.InternalTransaction + alias Explorer.Chain.{InternalTransaction, Wei} + alias Explorer.Factory + + import EthereumJSONRPC, only: [integer_to_quantity: 1] doctest InternalTransaction @@ -54,4 +57,186 @@ defmodule Explorer.Chain.InternalTransactionTest do assert Repo.insert(changeset) end end + + defp call_type(opts) do + defaults = [ + type: :call, + call_type: :call, + to_address_hash: Factory.address_hash(), + from_address_hash: Factory.address_hash(), + input: Factory.transaction_input(), + output: Factory.transaction_input(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + defp create_type(opts) do + defaults = [ + type: :create, + from_address_hash: Factory.address_hash(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + init: Factory.transaction_input(), + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + defp selfdestruct_type(opts) do + defaults = [ + type: :selfdestruct, + from_address_hash: Factory.address_hash(), + to_address_hash: Factory.address_hash(), + gas: Decimal.new(50_000), + gas_used: Decimal.new(25_000), + value: %Wei{value: 100}, + index: 0, + trace_address: [] + ] + + struct!(InternalTransaction, Keyword.merge(defaults, opts)) + end + + describe "internal_transactions_to_raw" do + test "it adds subtrace count" do + transactions = [ + call_type(trace_address: []), + call_type(trace_address: [0]), + call_type(trace_address: [1]), + call_type(trace_address: [2]), + call_type(trace_address: [0, 0]), + call_type(trace_address: [0, 1]), + call_type(trace_address: [1, 0]), + call_type(trace_address: [0, 0, 0]), + call_type(trace_address: [0, 0, 1]), + call_type(trace_address: [0, 0, 2]), + call_type(trace_address: [0, 1, 0]), + call_type(trace_address: [0, 1, 1]) + ] + + subtraces = + transactions + |> InternalTransaction.internal_transactions_to_raw() + |> Enum.map(&Map.get(&1, "subtraces")) + + assert subtraces == [3, 2, 1, 0, 3, 2, 0, 0, 0, 0, 0, 0] + end + + test "it correctly formats a call" do + from = Factory.address_hash() + to = Factory.address_hash() + gas = 50_000 + gas_used = 25_000 + input = Factory.transaction_input() + value = 50 + output = Factory.transaction_input() + + call_transaction = + call_type( + from_address_hash: from, + to_address_hash: to, + gas: Decimal.new(gas), + gas_used: Decimal.new(gas_used), + input: input, + value: %Wei{value: value}, + output: output + ) + + [call] = InternalTransaction.internal_transactions_to_raw([call_transaction]) + + assert call == %{ + "action" => %{ + "callType" => "call", + "from" => to_string(from), + "gas" => integer_to_quantity(gas), + "input" => to_string(input), + "to" => to_string(to), + "value" => integer_to_quantity(value) + }, + "result" => %{ + "gasUsed" => integer_to_quantity(gas_used), + "output" => to_string(output) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "call" + } + end + + test "it correctly formats a create" do + contract_code = Factory.contract_code_info().bytecode + contract_address = Factory.address_hash() + from = Factory.address_hash() + gas = 50_000 + gas_used = 25_000 + init = Factory.transaction_input() + value = 50 + + create_transaction = + create_type( + from_address_hash: from, + created_contract_code: contract_code, + created_contract_address_hash: contract_address, + gas: Decimal.new(gas), + gas_used: Decimal.new(gas_used), + init: init, + value: %Wei{value: value} + ) + + [create] = InternalTransaction.internal_transactions_to_raw([create_transaction]) + + assert create == %{ + "action" => %{ + "from" => to_string(from), + "gas" => integer_to_quantity(gas), + "init" => to_string(init), + "value" => integer_to_quantity(value) + }, + "result" => %{ + "address" => to_string(contract_address), + "code" => to_string(contract_code), + "gasUsed" => integer_to_quantity(gas_used) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "create" + } + end + + test "it correctly formats a selfdestruct" do + from_address = Factory.address_hash() + to_address = Factory.address_hash() + + value = 50 + + selfdestruct_transaction = + selfdestruct_type( + to_address_hash: to_address, + from_address_hash: from_address, + value: %Wei{value: value} + ) + + [selfdestruct] = InternalTransaction.internal_transactions_to_raw([selfdestruct_transaction]) + + assert selfdestruct == %{ + "action" => %{ + "address" => to_string(from_address), + "balance" => integer_to_quantity(value), + "refundAddress" => to_string(to_address) + }, + "subtraces" => 0, + "traceAddress" => [], + "type" => "suicide" + } + end + end end diff --git a/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs b/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs index 1d44a7adce..d1bed67de1 100644 --- a/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs +++ b/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs @@ -19,6 +19,7 @@ defmodule Explorer.ExchangeRatesTest do Application.put_env(:explorer, Explorer.ExchangeRates.Source, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) + Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true) on_exit(fn -> Application.put_env(:explorer, Explorer.ExchangeRates.Source, source_configuration) @@ -135,4 +136,10 @@ defmodule Explorer.ExchangeRatesTest do assert z == ExchangeRates.lookup("z") assert nil == ExchangeRates.lookup("nope") end + + test "lookup when disabled" do + Application.put_env(:explorer, Explorer.ExchangeRates, enabled: false) + + assert nil == ExchangeRates.lookup("z") + end end diff --git a/apps/explorer/test/explorer/known_tokens/known_tokens_test.exs b/apps/explorer/test/explorer/known_tokens/known_tokens_test.exs index 7d5cb21407..9076cc1d13 100644 --- a/apps/explorer/test/explorer/known_tokens/known_tokens_test.exs +++ b/apps/explorer/test/explorer/known_tokens/known_tokens_test.exs @@ -21,6 +21,7 @@ defmodule Explorer.KnownTokensTest do Application.put_env(:explorer, Explorer.KnownTokens.Source, source: TestSource) Application.put_env(:explorer, Explorer.KnownTokens, table_name: :known_tokens) + Application.put_env(:explorer, Explorer.KnownTokens, enabled: true) on_exit(fn -> Application.put_env(:explorer, Explorer.KnownTokens.Source, source_configuration) @@ -128,4 +129,10 @@ defmodule Explorer.KnownTokensTest do assert Hash.Address.cast("0x0000000000000000000000000000000000000001") == KnownTokens.lookup("TEST1") assert nil == KnownTokens.lookup("nope") end + + test "lookup when disabled" do + Application.put_env(:explorer, Explorer.KnownTokens, enabled: false) + + assert nil == KnownTokens.lookup("z") + end end diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs new file mode 100644 index 0000000000..ac6a600722 --- /dev/null +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -0,0 +1,238 @@ +defmodule Explorer.Token.PoolsReaderTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + alias Explorer.Staking.PoolsReader + + import Mox + + setup :verify_on_exit! + setup :set_mox_global + + describe "get_pools_list" do + test "get_active_pools success" do + get_pools_from_blockchain() + + result = PoolsReader.get_active_pools() + + assert Enum.count(result) == 3 + end + + test "get_active_pools error" do + fetch_from_blockchain_with_error() + + assert_raise MatchError, fn -> + PoolsReader.get_active_pools() + end + end + end + + describe "get_pools_data" do + test "get_pool_data success" do + get_pool_data_from_blockchain() + + address = <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>> + + response = { + :ok, + %{ + banned_until: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + mining_address: + <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>, + staked_amount: 0, + staking_address: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, + was_banned_count: 0, + was_validator_count: 2 + } + } + + assert PoolsReader.pool_data(address) == response + end + + test "get_pool_data error" do + fetch_from_blockchain_with_error() + + address = <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>> + + assert :error = PoolsReader.pool_data(address) + end + end + + defp get_pools_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: _}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6000000000000000000000000aa94b687d3f9552a453b81b2834ca53778980dc0000000000000000000000000312c230e7d6db05224f60208a656e3541c5c42ba" + } + ]} + end + ) + end + + defp fetch_from_blockchain_with_error() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: _}], _options -> + {:ok, + [ + %{ + error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, + id: id, + jsonrpc: "2.0" + } + ]} + end + ) + end + + defp get_pool_data_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + 2, + fn requests, _opts -> + {:ok, + Enum.map(requests, fn + # miningByStakingAddress + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x005351750000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78" + } + + # isPoolActive + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xa711e6a10000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + # poolDelegators + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x9ea8082b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + } + + # stakeAmountTotalMinusOrderedWithdraw + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x234fbf2b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # isValidator + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xfacd743b000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + # validatorCounter + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xb41832e4000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000002" + } + + # isValidatorBanned + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xa92252ae000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # bannedUntil + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x5836d08a000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # banCounter + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x1d0cd4c6000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + end)} + end + ) + end +end diff --git a/apps/indexer/README.md b/apps/indexer/README.md index 3821280b21..d0844c57d6 100644 --- a/apps/indexer/README.md +++ b/apps/indexer/README.md @@ -57,6 +57,7 @@ The following async fetchers are launched for importing missing data: - `token_balance` - `token` - `contract_code` +- `staking_pools` ### Async fetchers @@ -78,6 +79,7 @@ Most of them are based off `BufferedTask`, and the basic algorithm goes like thi - `token_balance`: for `address_token_balances` with null `value_fetched_at`. Also upserts `address_current_token_balances` - `token`: for `tokens` with `cataloged == false` - `contract_code`: for `transactions` with non-null `created_contract_address_hash` and null `created_contract_code_indexed_at` +- `staking_pools`: for fetching staking pools Additionally: - `token_updater` is run every 2 days to update token metadata diff --git a/apps/indexer/config/config.exs b/apps/indexer/config/config.exs index 181323c0a3..04c2edf7c8 100644 --- a/apps/indexer/config/config.exs +++ b/apps/indexer/config/config.exs @@ -38,6 +38,7 @@ config :indexer, # config :indexer, Indexer.Fetcher.ReplacedTransaction.Supervisor, disabled?: true # config :indexer, Indexer.Fetcher.BlockReward.Supervisor, disabled?: true +config :indexer, Indexer.Fetcher.StakingPools.Supervisor, disabled?: true config :indexer, Indexer.Tracer, service: :indexer, diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index ad986c9ba0..deb0e4554f 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -20,6 +20,7 @@ defmodule Indexer.Block.Fetcher do ContractCode, InternalTransaction, ReplacedTransaction, + StakingPools, Token, TokenBalance, UncleBlock @@ -280,6 +281,10 @@ defmodule Indexer.Block.Fetcher do def async_import_token_balances(_), do: :ok + def async_import_staking_pools do + StakingPools.async_fetch() + end + def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do UncleBlock.async_fetch_blocks(block_second_degree_relations) end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 9fce118964..8778677f33 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -20,7 +20,8 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_tokens: 1, async_import_token_balances: 1, async_import_uncles: 1, - fetch_and_import_range: 2 + fetch_and_import_range: 2, + async_import_staking_pools: 0 ] alias Ecto.Changeset @@ -350,6 +351,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_token_balances(imported) async_import_uncles(imported) async_import_replaced_transactions(imported) + async_import_staking_pools() end defp balances( diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex new file mode 100644 index 0000000000..68794d9ee4 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex @@ -0,0 +1,136 @@ +defmodule Indexer.Fetcher.StakingPools do + @moduledoc """ + Fetches staking pools and send to be imported in `Address.Name` table + """ + + use Indexer.Fetcher + use Spandex.Decorators + + require Logger + + alias Explorer.Chain + alias Explorer.Staking.PoolsReader + alias Indexer.BufferedTask + alias Indexer.Fetcher.StakingPools.Supervisor, as: StakingPoolsSupervisor + + @behaviour BufferedTask + + @defaults [ + flush_interval: 300, + max_batch_size: 100, + max_concurrency: 10, + task_supervisor: Indexer.Fetcher.StakingPools.TaskSupervisor + ] + + @max_retries 3 + + @spec async_fetch() :: :ok + def async_fetch do + if StakingPoolsSupervisor.disabled?() do + :ok + else + pools = + PoolsReader.get_pools() + |> Enum.map(&entry/1) + + BufferedTask.buffer(__MODULE__, pools, :infinity) + end + end + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + @defaults + |> Keyword.merge(init_options) + |> Keyword.put(:state, {0, []}) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(_initial, reducer, acc) do + PoolsReader.get_pools() + |> Enum.map(&entry/1) + |> Enum.reduce(acc, &reducer.(&1, &2)) + end + + @impl BufferedTask + def run(pools, _json_rpc_named_arguments) do + failed_list = + pools + |> Enum.map(&Map.put(&1, :retries_count, &1.retries_count + 1)) + |> fetch_from_blockchain() + |> import_pools() + + if failed_list == [] do + :ok + else + {:retry, failed_list} + end + end + + def entry(pool_address) do + %{ + staking_address: pool_address, + retries_count: 0 + } + end + + defp fetch_from_blockchain(addresses) do + addresses + |> Enum.filter(&(&1.retries_count <= @max_retries)) + |> Enum.map(fn %{staking_address: staking_address} = pool -> + case PoolsReader.pool_data(staking_address) do + {:ok, data} -> + Map.merge(pool, data) + + error -> + Map.put(pool, :error, error) + end + end) + end + + defp import_pools(pools) do + {failed, success} = + Enum.reduce(pools, {[], []}, fn + %{error: _error, staking_address: address}, {failed, success} -> + {[address | failed], success} + + pool, {failed, success} -> + {failed, [changeset(pool) | success]} + end) + + import_params = %{ + staking_pools: %{params: success}, + timeout: :infinity + } + + case Chain.import(import_params) do + {:ok, _} -> + :ok + + {:error, reason} -> + Logger.debug(fn -> ["failed to import staking pools: ", inspect(reason)] end, + error_count: Enum.count(pools) + ) + end + + failed + end + + defp changeset(%{staking_address: staking_address} = pool) do + {:ok, mining_address} = Chain.Hash.Address.cast(pool[:mining_address]) + + data = + pool + |> Map.delete(:staking_address) + |> Map.put(:mining_address, mining_address) + + %{ + name: "anonymous", + primary: true, + address_hash: staking_address, + metadata: data + } + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 73be10f098..375a17e669 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -16,6 +16,7 @@ defmodule Indexer.Supervisor do InternalTransaction, PendingTransaction, ReplacedTransaction, + StakingPools, Token, TokenBalance, TokenUpdater, @@ -122,6 +123,7 @@ defmodule Indexer.Supervisor do {TokenBalance.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, + {StakingPools.Supervisor, [[memory_monitor: memory_monitor]]}, # Out-of-band fetchers {CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]}, diff --git a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs new file mode 100644 index 0000000000..8f985537bf --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs @@ -0,0 +1,205 @@ +defmodule Indexer.Fetcher.StakingPoolsTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + + alias Indexer.Fetcher.StakingPools + alias Explorer.Staking.PoolsReader + alias Explorer.Chain.Address + + @moduletag :capture_log + + setup :verify_on_exit! + + describe "init/3" do + test "returns pools addresses" do + get_pools_from_blockchain(2) + + list = StakingPools.init([], &[&1 | &2], []) + + assert Enum.count(list) == 6 + end + end + + describe "run/3" do + test "one success import from pools" do + get_pools_from_blockchain(1) + + list = + PoolsReader.get_active_pools() + |> Enum.map(&StakingPools.entry/1) + + success_address = + list + |> List.first() + |> Map.get(:staking_address) + + get_pool_data_from_blockchain() + + assert {:retry, retry_list} = StakingPools.run(list, nil) + assert Enum.count(retry_list) == 2 + + pool = Explorer.Repo.get_by(Address.Name, address_hash: success_address) + assert pool.name == "anonymous" + end + end + + defp get_pools_from_blockchain(n) do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + n, + fn [%{id: id, method: "eth_call", params: _}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6000000000000000000000000aa94b687d3f9552a453b81b2834ca53778980dc0000000000000000000000000312c230e7d6db05224f60208a656e3541c5c42ba" + } + ]} + end + ) + end + + defp get_pool_data_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + 4, + fn requests, _opts -> + {:ok, + Enum.map(requests, fn + # miningByStakingAddress + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x005351750000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78" + } + + # isPoolActive + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xa711e6a10000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + # poolDelegators + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x9ea8082b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + } + + # stakeAmountTotalMinusOrderedWithdraw + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x234fbf2b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # isValidator + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xfacd743b000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + + # validatorCounter + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xb41832e4000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000002" + } + + # isValidatorBanned + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0xa92252ae000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # bannedUntil + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x5836d08a000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # banCounter + %{ + id: id, + method: "eth_call", + params: [ + %{data: "0x1d0cd4c6000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + end)} + end + ) + end +end