Merge branch 'master' into master

pull/1874/head
maxgrapps 6 years ago committed by GitHub
commit 8da365b462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 2
      apps/block_scout_web/assets/css/_code.scss
  3. 7
      apps/block_scout_web/assets/js/view_specific/raw_trace/code_highlighting.js
  4. 55
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex
  5. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  6. 5
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex
  7. 1
      apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/_metatags.html.eex
  8. 18
      apps/block_scout_web/lib/block_scout_web/templates/transaction_raw_trace/index.html.eex
  9. 49
      apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
  10. 18
      apps/block_scout_web/lib/block_scout_web/views/transaction_raw_trace_view.ex
  11. 3
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  12. 7
      apps/block_scout_web/priv/gettext/default.pot
  13. 7
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  14. 1
      apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs
  15. 1
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs
  16. 16
      apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs
  17. 88
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex
  18. 36
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex
  19. 282
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex
  20. 47
      apps/ethereum_jsonrpc/priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js
  21. 4
      apps/explorer/config/config.exs
  22. 4
      apps/explorer/lib/explorer/application.ex
  23. 88
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  24. 3
      apps/explorer/lib/explorer/chain/import/runner/transactions.ex
  25. 3
      apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex
  26. 119
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  27. 42
      apps/explorer/lib/explorer/chain/internal_transaction/action.ex
  28. 32
      apps/explorer/lib/explorer/chain/internal_transaction/result.ex
  29. 6
      apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex
  30. 10
      apps/explorer/lib/explorer/known_tokens/known_tokens.ex
  31. 121
      apps/explorer/lib/explorer/staking/pools_reader.ex
  32. 2
      apps/explorer/lib/explorer/validator/metadata_retriever.ex
  33. 0
      apps/explorer/priv/contracts_abi/poa/metadata.json
  34. 0
      apps/explorer/priv/contracts_abi/poa/validators.json
  35. 925
      apps/explorer/priv/contracts_abi/pos/staking.json
  36. 492
      apps/explorer/priv/contracts_abi/pos/validators.json
  37. 94
      apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
  38. 187
      apps/explorer/test/explorer/chain/internal_transaction_test.exs
  39. 7
      apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs
  40. 7
      apps/explorer/test/explorer/known_tokens/known_tokens_test.exs
  41. 238
      apps/explorer/test/explorer/staking/pools_reader_test.exs
  42. 2
      apps/indexer/README.md
  43. 1
      apps/indexer/config/config.exs
  44. 5
      apps/indexer/lib/indexer/block/fetcher.ex
  45. 4
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  46. 136
      apps/indexer/lib/indexer/fetcher/staking_pools.ex
  47. 2
      apps/indexer/lib/indexer/supervisor.ex
  48. 205
      apps/indexer/test/indexer/fetcher/staking_pools_test.exs

@ -5,6 +5,8 @@
- [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x" - [#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 - [#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 - [#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 ### Fixes
@ -13,7 +15,9 @@
- [#1840](https://github.com/poanetwork/blockscout/pull/1840) - Handle case when total supply is nil - [#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 - [#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 - [#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 - [#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 ### Chore
@ -31,6 +35,7 @@
- [#1777](https://github.com/poanetwork/blockscout/pull/1777) - show ERC-20 token transfer info on transaction page - [#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 - [#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 - [#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 ### Fixes

@ -14,7 +14,7 @@ pre {
.pre-decompiled code::before { .pre-decompiled code::before {
content: counter(line); content: counter(line);
display: inline-block; display: inline-block;
width: flex; width: 3em;
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
padding: 0 .5em; padding: 0 .5em;
margin-right: .5em; margin-right: .5em;

@ -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()
}

@ -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

@ -99,6 +99,13 @@ defmodule BlockScoutWeb.Router do
as: :internal_transaction as: :internal_transaction
) )
resources(
"/raw_trace",
TransactionRawTraceController,
only: [:index],
as: :raw_trace
)
resources("/logs", TransactionLogController, only: [:index], as: :log) resources("/logs", TransactionLogController, only: [:index], as: :log)
resources("/token_transfers", TransactionTokenTransferController, resources("/token_transfers", TransactionTokenTransferController,

@ -20,4 +20,9 @@
"data-test": "transaction_logs_link" "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)
) %>
</div> </div>

@ -0,0 +1 @@
<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %>

@ -0,0 +1,18 @@
<section class="container">
<%= render BlockScoutWeb.TransactionView, "overview.html", assigns %>
<div class="card">
<div class="card-header">
<%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %>
</div>
<div class="card-body">
<h2 class="card-title"><%= gettext "Raw Trace" %></h2>
<%= if Enum.count(@internal_transactions) > 0 do %>
<pre class="pre-scrollable line-numbers" data-activate-highlight><code class="json "><%= for {line, number} <- raw_traces_with_lines(@internal_transactions) do %><div data-line-number="<%= number %>"><%= line %></div><% end %></code></pre>
<% else %>
No trace entries found.
<% end %>
</div>
</div>
</section>

@ -18,6 +18,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
} }
def highlight_decompiled_code(code) do def highlight_decompiled_code(code) do
{_, result} =
@colors @colors
|> Enum.reduce(code, fn {symbol, rgb}, acc -> |> Enum.reduce(code, fn {symbol, rgb}, acc ->
String.replace(acc, symbol, "<span style=\"color:rgb(#{rgb})\">") String.replace(acc, symbol, "<span style=\"color:rgb(#{rgb})\">")
@ -25,6 +26,24 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
|> String.replace("\e[1m", "<span style=\"font-weight:bold\">") |> String.replace("\e[1m", "<span style=\"font-weight:bold\">")
|> String.replace("»", "&raquo;") |> String.replace("»", "&raquo;")
|> String.replace("\e[0m", "</span>") |> String.replace("\e[0m", "</span>")
|> String.split(~r/\<span style=.*?\)"\>|\<\/span\>/, include_captures: true, trim: true)
|> Enum.reduce({"", []}, fn part, {style, acc} ->
new_style =
cond do
String.contains?(part, "<span style") -> part
part == "</span>" -> ""
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)
|> add_line_numbers() |> add_line_numbers()
end end
@ -41,4 +60,34 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
acc <> "<code>#{line}</code>\n" acc <> "<code>#{line}</code>\n"
end) end)
end end
defp new_part(part, new_style) do
cond do
part == "" ->
""
part == "</span>" ->
""
part == new_style ->
""
new_style == "" ->
part
true ->
result =
part
|> String.split("\n")
|> Enum.reduce("", fn p, a ->
a <> new_style <> p <> "</span>\n"
end)
if String.ends_with?(part, "\n") do
result
else
String.slice(result, 0..-2)
end
end
end
end end

@ -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

@ -13,7 +13,7 @@ defmodule BlockScoutWeb.TransactionView do
import BlockScoutWeb.Gettext import BlockScoutWeb.Gettext
import BlockScoutWeb.Tokens.Helpers 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] 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(["token_transfers"]), do: gettext("Token Transfers")
defp tab_name(["internal_transactions"]), do: gettext("Internal Transactions") defp tab_name(["internal_transactions"]), do: gettext("Internal Transactions")
defp tab_name(["logs"]), do: gettext("Logs") defp tab_name(["logs"]), do: gettext("Logs")
defp tab_name(["raw_trace"]), do: gettext("Raw Trace")
defp decode_params(params, types) do defp decode_params(params, types) do
params params

@ -1732,3 +1732,10 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:225 #: lib/block_scout_web/templates/transaction/overview.html.eex:225
msgid "Gas" msgid "Gas"
msgstr "" 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 ""

@ -1732,3 +1732,10 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:225 #: lib/block_scout_web/templates/transaction/overview.html.eex:225
msgid "Gas" msgid "Gas"
msgstr "" 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 ""

@ -16,6 +16,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
configuration = Application.get_env(:explorer, Explorer.ExchangeRates) configuration = Application.get_env(:explorer, Explorer.ExchangeRates)
Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource)
Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates)
Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true)
ExchangeRates.init([]) ExchangeRates.init([])

@ -107,6 +107,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
configuration = Application.get_env(:explorer, Explorer.ExchangeRates) configuration = Application.get_env(:explorer, Explorer.ExchangeRates)
Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource)
Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates)
Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true)
ExchangeRates.init([]) ExchangeRates.init([])

@ -56,7 +56,21 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do
result = AddressDecompiledContractView.highlight_decompiled_code(code) result = AddressDecompiledContractView.highlight_decompiled_code(code)
assert result == assert result ==
"<code> <span style=\"color:rgb(111, 110, 111)\">#</code>\n<code> # eveem.org 6 Feb 2019</code>\n<code> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></code>\n<code> #</code>\n<code> # Let's make the world open source</code>\n<code> # </span></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</code>\n<code> # I failed with these:</code>\n<code> </span><span style=\"color:rgb(111, 110, 111)\"># - </span><span style=\"color:rgb(236, 89, 58)\">unknowne77c646d(?)</span><span style=\"color:rgb(111, 110, 111)\"></code>\n<code> </span><span style=\"color:rgb(111, 110, 111)\"># - </span><span style=\"color:rgb(236, 89, 58)\">transferFromWithData(address _from, address _to, uint256 _value, bytes _data)</span><span style=\"color:rgb(111, 110, 111)\"></code>\n<code> # All the rest is below.</code>\n<code> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># Storage definitions and getters</span></code>\n<code></code>\n<code> <span style=\"color:rgb(57, 115, 0)\">def</span> storage:</code>\n<code> <span style=\"color:rgb(57, 115, 0)\">allowance</span> is uint256 => uint256 <span style=\"color:rgb(111, 110, 111)\"># mask(256, 0) at storage #2</span></code>\n<code> <span style=\"color:rgb(57, 115, 0)\">stor4</span> is uint256 => uint8 <span style=\"color:rgb(111, 110, 111)\"># mask(8, 0) at storage #4</span></code>\n<code></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>allowance(address <span style=\"color:rgb(57, 115, 0)\">_owner</span>, address <span style=\"color:rgb(57, 115, 0)\">_spender</span>) <span style=\"color:rgb(136, 0, 0)\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code> require (calldata.size - 4)<span style=\"font-weight:bold\"> >= </span>64</code>\n<code> return <span style=\"color:rgb(57, 115, 0)\">allowance</span><span style=\"color:rgb(57, 115, 0)\">[</span>sha3(((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span><span style=\"color:rgb(57, 115, 0)\">_owner</span>), 1), ((320 - 1)<span style=\"font-weight:bold\"> and </span><span style=\"color:rgb(57, 115, 0)\">_spender</span><span style=\"font-weight:bold\"> and </span>(320 - 1))<span style=\"color:rgb(57, 115, 0)\">]</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</code>\n<code> # Regular functions - see Tutorial for understanding quirks of the code</code>\n<code> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># folder failed in this function - may be terribly long, sorry</span></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>unknownc47d033b(?) <span style=\"color:rgb(136, 0, 0)\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code> if (calldata.size - 4)<span style=\"font-weight:bold\"> < </span>32:</code>\n<code> revert</code>\n<code> else:</code>\n<code> if not (320 - 1)<span style=\"font-weight:bold\"> or </span>not cd[4]:</code>\n<code> revert</code>\n<code> else:</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>0<span style=\"color:rgb(136, 0, 0)\">]</span> = (320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4]</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>32<span style=\"color:rgb(136, 0, 0)\">]</span> = 4</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>96<span style=\"color:rgb(136, 0, 0)\">]</span> = bool(<span style=\"color:rgb(57, 115, 0)\">stor4</span><span style=\"color:rgb(57, 115, 0)\">[</span>((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])<span style=\"color:rgb(57, 115, 0)\">]</span>)</code>\n<code> return bool(<span style=\"color:rgb(57, 115, 0)\">stor4</span><span style=\"color:rgb(57, 115, 0)\">[</span>((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])<span style=\"color:rgb(57, 115, 0)\">]</span>)</code>\n<code></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>_fallback() <span style=\"color:rgb(136, 0, 0)\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"># default function</span></code>\n<code> revert</code>\n<code></code>\n" "<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # eveem.org 6 Feb 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # </span></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # I failed with these:</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span><span style=\"color:rgb(236, 89, 58)\">unknowne77c646d(?)</span><span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span><span style=\"color:rgb(236, 89, 58)\">transferFromWithData(address _from, address _to, uint256 _value, bytes _data)</span><span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # All the rest is below.</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># Storage definitions and getters</span></code>\n<code></code>\n<code> <span style=\"color:rgb(57, 115, 0)\">def</span> storage:</code>\n<code> <span style=\"color:rgb(57, 115, 0)\">allowance</span> is uint256 => uint256 <span style=\"color:rgb(111, 110, 111)\"># mask(256, 0) at storage #2</span></code>\n<code> <span style=\"color:rgb(57, 115, 0)\">stor4</span> is uint256 => uint8 <span style=\"color:rgb(111, 110, 111)\"># mask(8, 0) at storage #4</span></code>\n<code></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>allowance(address <span style=\"color:rgb(57, 115, 0)\">_owner</span>, address <span style=\"color:rgb(57, 115, 0)\">_spender</span>) <span style=\"color:rgb(136, 0, 0)\">payable</span>: 64</code>\n<code> return <span style=\"color:rgb(57, 115, 0)\">allowance</span><span style=\"color:rgb(57, 115, 0)\">[</span>sha3(((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span><span style=\"color:rgb(57, 115, 0)\">_owner</span>), 1), ((320 - 1)<span style=\"font-weight:bold\"> and </span><span style=\"color:rgb(57, 115, 0)\">_spender</span><span style=\"font-weight:bold\"> and </span>(320 - 1))<span style=\"color:rgb(57, 115, 0)\">]</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Regular functions - see Tutorial for understanding quirks of the code</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># folder failed in this function - may be terribly long, sorry</span></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>unknownc47d033b(?) <span style=\"color:rgb(136, 0, 0)\">payable</span>: not cd[4]:</code>\n<code> revert</code>\n<code> else:</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>0<span style=\"color:rgb(136, 0, 0)\">]</span>cd[4]</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>32<span style=\"color:rgb(136, 0, 0)\">]</span> = 4</code>\n<code> <span style=\"color:rgb(136, 0, 0)\">mem[</span>96<span style=\"color:rgb(136, 0, 0)\">]</span> = bool(<span style=\"color:rgb(57, 115, 0)\">stor4</span><span style=\"color:rgb(57, 115, 0)\">[</span>((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])<span style=\"color:rgb(57, 115, 0)\">]</span>)</code>\n<code> return bool(<span style=\"color:rgb(57, 115, 0)\">stor4</span><span style=\"color:rgb(57, 115, 0)\">[</span>((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])<span style=\"color:rgb(57, 115, 0)\">]</span>)</code>\n<code></code>\n<code> <span style=\"color:rgb(136, 0, 0)\">def </span>_fallback() <span style=\"color:rgb(136, 0, 0)\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"># default function</span></code>\n<code> revert</code>\n<code></code>\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) ==
"<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # eveem.org 6 Feb 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # </span></code>\n<code></code>\n"
end end
end end

@ -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). 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 @behaviour EthereumJSONRPC.Variant
@ -28,7 +29,11 @@ defmodule EthereumJSONRPC.Geth do
id_to_params id_to_params
|> debug_trace_transaction_requests() |> debug_trace_transaction_requests()
|> json_rpc(json_rpc_named_arguments) do |> 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
end end
@ -62,13 +67,88 @@ defmodule EthereumJSONRPC.Geth do
request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]}) request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]})
end 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 when is_list(responses) and is_map(id_to_params) do
responses responses
|> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params)) |> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params))
|> reduce_internal_transactions_params() |> reduce_internal_transactions_params()
end 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) defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, result: calls}, id_to_params)
when is_map(id_to_params) do when is_map(id_to_params) do
%{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} = %{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} =

@ -328,9 +328,8 @@ defmodule EthereumJSONRPC.Geth.Call do
"from" => from_address_hash, "from" => from_address_hash,
"to" => to_address_hash, "to" => to_address_hash,
"gas" => gas, "gas" => gas,
"gasUsed" => gas_used,
"input" => input, "input" => input,
"output" => output, "error" => error,
"value" => value "value" => value
}) })
when call_type in ~w(call callcode delegatecall) do when call_type in ~w(call callcode delegatecall) do
@ -345,9 +344,8 @@ defmodule EthereumJSONRPC.Geth.Call do
from_address_hash: from_address_hash, from_address_hash: from_address_hash,
to_address_hash: to_address_hash, to_address_hash: to_address_hash,
gas: gas, gas: gas,
gas_used: gas_used,
input: input, input: input,
output: output, error: error,
value: value value: value
} }
end end
@ -363,8 +361,9 @@ defmodule EthereumJSONRPC.Geth.Call do
"from" => from_address_hash, "from" => from_address_hash,
"to" => to_address_hash, "to" => to_address_hash,
"gas" => gas, "gas" => gas,
"gasUsed" => gas_used,
"input" => input, "input" => input,
"error" => error, "output" => output,
"value" => value "value" => value
}) })
when call_type in ~w(call callcode delegatecall) do when call_type in ~w(call callcode delegatecall) do
@ -379,8 +378,9 @@ defmodule EthereumJSONRPC.Geth.Call do
from_address_hash: from_address_hash, from_address_hash: from_address_hash,
to_address_hash: to_address_hash, to_address_hash: to_address_hash,
gas: gas, gas: gas,
gas_used: gas_used,
input: input, input: input,
error: error, output: output,
value: value value: value
} }
end end
@ -425,13 +425,11 @@ defmodule EthereumJSONRPC.Geth.Call do
"transactionHash" => transaction_hash, "transactionHash" => transaction_hash,
"index" => index, "index" => index,
"traceAddress" => trace_address, "traceAddress" => trace_address,
"type" => "create", "type" => "create" = type,
"from" => from_address_hash, "from" => from_address_hash,
"createdContractAddressHash" => created_contract_address_hash, "error" => error,
"gas" => gas, "gas" => gas,
"gasUsed" => gas_used,
"init" => init, "init" => init,
"createdContractCode" => created_contract_code,
"value" => value "value" => value
}) do }) do
%{ %{
@ -440,13 +438,11 @@ defmodule EthereumJSONRPC.Geth.Call do
transaction_hash: transaction_hash, transaction_hash: transaction_hash,
index: index, index: index,
trace_address: trace_address, trace_address: trace_address,
type: "create", type: type,
from_address_hash: from_address_hash, from_address_hash: from_address_hash,
gas: gas, gas: gas,
gas_used: gas_used, error: error,
created_contract_address_hash: created_contract_address_hash,
init: init, init: init,
created_contract_code: created_contract_code,
value: value value: value
} }
end end
@ -457,11 +453,13 @@ defmodule EthereumJSONRPC.Geth.Call do
"transactionHash" => transaction_hash, "transactionHash" => transaction_hash,
"index" => index, "index" => index,
"traceAddress" => trace_address, "traceAddress" => trace_address,
"type" => "create" = type, "type" => "create",
"from" => from_address_hash, "from" => from_address_hash,
"error" => error, "createdContractAddressHash" => created_contract_address_hash,
"gas" => gas, "gas" => gas,
"gasUsed" => gas_used,
"init" => init, "init" => init,
"createdContractCode" => created_contract_code,
"value" => value "value" => value
}) do }) do
%{ %{
@ -470,11 +468,13 @@ defmodule EthereumJSONRPC.Geth.Call do
transaction_hash: transaction_hash, transaction_hash: transaction_hash,
index: index, index: index,
trace_address: trace_address, trace_address: trace_address,
type: type, type: "create",
from_address_hash: from_address_hash, from_address_hash: from_address_hash,
gas: gas, gas: gas,
error: error, gas_used: gas_used,
created_contract_address_hash: created_contract_address_hash,
init: init, init: init,
created_contract_code: created_contract_code,
value: value value: value
} }
end end

@ -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

@ -73,6 +73,15 @@
topCall.calls.push(childCall); 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) { success(log, db) {
const op = log.op.toString(); const op = log.op.toString();
@ -115,8 +124,6 @@
// Pop off the last call and get the execution results // Pop off the last call and get the execution results
const call = this.callStack.pop(); const call = this.callStack.pop();
call.gasUsedBigInt = call.gasBigInt.subtract(log.getGas());
const ret = log.stack.peek(0); const ret = log.stack.peek(0);
if (!ret.equals(0)) { if (!ret.equals(0)) {
@ -124,7 +131,7 @@
call.createdContractAddressHash = toHex(toAddress(ret.toString(16))); call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));
call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16)))); call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));
} else { } 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) { } else if (call.error === undefined) {
call.error = 'internal failure'; call.error = 'internal failure';
@ -135,6 +142,9 @@
this.pushChildCall(call); this.pushChildCall(call);
} }
else {
this.pushGasToTopCall(log);
}
}, },
createOp(log) { createOp(log) {
@ -147,7 +157,6 @@
type: 'create', type: 'create',
from: toHex(log.contract.getAddress()), from: toHex(log.contract.getAddress()),
init: toHex(log.memory.slice(inputOffset, inputEnd)), init: toHex(log.memory.slice(inputOffset, inputEnd)),
gasBigInt: bigInt(log.getGas()),
valueBigInt: bigInt(stackValue.toString(10)) valueBigInt: bigInt(stackValue.toString(10))
}; };
this.callStack.push(call); this.callStack.push(call);
@ -160,7 +169,8 @@
type: 'selfdestruct', type: 'selfdestruct',
from: toHex(contractAddress), from: toHex(contractAddress),
to: toHex(toAddress(log.stack.peek(0).toString(16))), to: toHex(toAddress(log.stack.peek(0).toString(16))),
gasBigInt: bigInt(log.getGas()), gasBigInt: log.getGas(),
gasUsedBigInt: log.getCost(),
valueBigInt: db.getBalance(contractAddress) valueBigInt: db.getBalance(contractAddress)
}); });
}, },
@ -186,7 +196,6 @@
callType: op.toLowerCase(), callType: op.toLowerCase(),
from: toHex(log.contract.getAddress()), from: toHex(log.contract.getAddress()),
to: toHex(to), to: toHex(to),
gasBigInt: bigInt(log.getGas()),
input: toHex(log.memory.slice(inputOffset, inputEnd)), input: toHex(log.memory.slice(inputOffset, inputEnd)),
outputOffset: log.stack.peek(4 + stackOffset).valueOf(), outputOffset: log.stack.peek(4 + stackOffset).valueOf(),
outputLength: log.stack.peek(5 + stackOffset).valueOf() outputLength: log.stack.peek(5 + stackOffset).valueOf()
@ -220,7 +229,7 @@
result(ctx, db) { result(ctx, db) {
const result = this.ctxToResult(ctx, db); const result = this.ctxToResult(ctx, db);
const filtered = this.filterNotUndefined(result); 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); return this.encodeCallSequence(callSequence);
}, },
@ -339,7 +348,7 @@
}, },
// sequence converts the finalized calls from a call tree to a call sequence // 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; const subcalls = call.calls;
delete call.calls; delete call.calls;
@ -347,38 +356,24 @@
if (call.type === 'call' && call.callType === 'delegatecall') { if (call.type === 'call' && call.callType === 'delegatecall') {
call.valueBigInt = availableValueBigInt; call.valueBigInt = availableValueBigInt;
} else if (call.type === 'selfdestruct') {
call.gasUsedBigInt = availableGasBigInt
} }
var newCallSequence = callSequence.concat([call]); var newCallSequence = callSequence.concat([call]);
if (subcalls !== undefined) { if (subcalls !== undefined) {
var nestedAvailableValueBigInt = availableValueBigInt;
var nestedAvailableGasBigInt = availableGasBigInt;
for (var i = 0; i < subcalls.length; i++) { for (var i = 0; i < subcalls.length; i++) {
const nestedSequenced = this.sequence( const nestedSequenced = this.sequence(
subcalls[i], subcalls[i],
newCallSequence, newCallSequence,
nestedAvailableValueBigInt, call.valueBigInt,
availableGasBigInt,
traceAddress.concat([i]) traceAddress.concat([i])
); );
newCallSequence = nestedSequenced.callSequence; newCallSequence = nestedSequenced.callSequence;
nestedAvailableValueBigInt = nestedSequenced.availableValueBigInt;
nestedAvailableGasBigInt = nestedSequenced.availableGasBigInt;
} }
} }
const newAvailableValueBigInt = availableValueBigInt.subtract(call.valueBigInt);
const newAvailableGasUsedBigInt = availableGasBigInt.subtract(call.gasUsedBigInt);
return { return {
callSequence: newCallSequence, callSequence: newCallSequence
availableValueBigInt: newAvailableValueBigInt,
availableGasBigInt: newAvailableGasUsedBigInt
}; };
}, },
@ -410,7 +405,7 @@
delete call.gasBigInt; delete call.gasBigInt;
if (gasBigInt === undefined) { if (gasBigInt === undefined) {
throw "gasBigInt undefined in " + JSON.stringify(call); gasBigInt = bigInt.zero;
} }
call.gas = '0x' + gasBigInt.toString(16); call.gas = '0x' + gasBigInt.toString(16);
@ -421,7 +416,7 @@
delete call.gasUsedBigInt; delete call.gasUsedBigInt;
if (gasUsedBigInt === undefined) { if (gasUsedBigInt === undefined) {
throw "gasUsedBigInt undefined in " + JSON.stringify(call); gasUsedBigInt = bigInt.zero;
} }
call.gasUsed = '0x' + gasUsedBigInt.toString(16); call.gasUsed = '0x' + gasUsedBigInt.toString(16);

@ -54,6 +54,10 @@ else
config :explorer, Explorer.Validator.MetadataProcessor, enabled: false config :explorer, Explorer.Validator.MetadataProcessor, enabled: false
end 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 if System.get_env("SUPPLY_MODULE") == "TokenBridge" do
config :explorer, supply: Explorer.Chain.Supply.TokenBridge config :explorer, supply: Explorer.Chain.Supply.TokenBridge
end end

@ -56,9 +56,7 @@ defmodule Explorer.Application do
end end
defp should_start?(process) do defp should_start?(process) do
:explorer Application.get_env(:explorer, process, [])[:enabled] == true
|> Application.fetch_env!(process)
|> Keyword.fetch!(:enabled)
end end
defp configure(process) do defp configure(process) do

@ -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

@ -86,7 +86,8 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
conflict_target: :hash, conflict_target: :hash,
on_conflict: on_conflict, on_conflict: on_conflict,
for: Transaction, 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, timeout: timeout,
timestamps: timestamps timestamps: timestamps
) )

@ -24,7 +24,8 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
Runner.Tokens, Runner.Tokens,
Runner.TokenTransfers, Runner.TokenTransfers,
Runner.Address.CurrentTokenBalances, Runner.Address.CurrentTokenBalances,
Runner.Address.TokenBalances Runner.Address.TokenBalances,
Runner.StakingPools
] ]
@impl Stage @impl Stage

@ -4,7 +4,7 @@ defmodule Explorer.Chain.InternalTransaction do
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Address, Data, Gas, Hash, Transaction, Wei} alias Explorer.Chain.{Address, Data, Gas, Hash, Transaction, Wei}
alias Explorer.Chain.InternalTransaction.{CallType, Type} alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type}
@typedoc """ @typedoc """
* `block_number` - the `t:Explorer.Chain.Block.t/0` `number` that the `transaction` is collated into. * `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 def where_block_number_is_not_null(query) do
where(query, [t], not is_nil(t.block_number)) where(query, [t], not is_nil(t.block_number))
end 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 end

@ -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

@ -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

@ -90,7 +90,7 @@ defmodule Explorer.ExchangeRates do
""" """
@spec lookup(String.t()) :: Token.t() | nil @spec lookup(String.t()) :: Token.t() | nil
def lookup(symbol) do def lookup(symbol) do
if store() == :ets do if store() == :ets && enabled?() do
case :ets.lookup(table_name(), symbol) do case :ets.lookup(table_name(), symbol) do
[tuple | _] when is_tuple(tuple) -> Token.from_tuple(tuple) [tuple | _] when is_tuple(tuple) -> Token.from_tuple(tuple)
_ -> nil _ -> nil
@ -133,4 +133,8 @@ defmodule Explorer.ExchangeRates do
defp store do defp store do
config(:store) || :ets config(:store) || :ets
end end
defp enabled? do
Application.get_env(:explorer, __MODULE__, [])[:enabled] == true
end
end end

@ -81,7 +81,11 @@ defmodule Explorer.KnownTokens do
""" """
@spec list :: [{String.t(), Hash.Address.t()}] @spec list :: [{String.t(), Hash.Address.t()}]
def list do def list do
if enabled?() do
list_from_store(store()) list_from_store(store())
else
[]
end
end end
@doc """ @doc """
@ -89,7 +93,7 @@ defmodule Explorer.KnownTokens do
""" """
@spec lookup(String.t()) :: {:ok, Hash.Address.t()} | :error | nil @spec lookup(String.t()) :: {:ok, Hash.Address.t()} | :error | nil
def lookup(symbol) do def lookup(symbol) do
if store() == :ets do if store() == :ets && enabled?() do
case :ets.lookup(table_name(), symbol) do case :ets.lookup(table_name(), symbol) do
[{_symbol, address} | _] -> Hash.Address.cast(address) [{_symbol, address} | _] -> Hash.Address.cast(address)
_ -> nil _ -> nil
@ -128,4 +132,8 @@ defmodule Explorer.KnownTokens do
defp store do defp store do
config(:store) || :ets config(:store) || :ets
end end
defp enabled? do
Application.get_env(:explorer, __MODULE__, [])[:enabled] == true
end
end end

@ -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

@ -69,7 +69,7 @@ defmodule Explorer.Validator.MetadataRetriever do
# sobelow_skip ["Traversal"] # sobelow_skip ["Traversal"]
defp contract_abi(file_name) do defp contract_abi(file_name) do
:explorer :explorer
|> Application.app_dir("priv/validator_contracts_abi/#{file_name}") |> Application.app_dir("priv/contracts_abi/poa/#{file_name}")
|> File.read!() |> File.read!()
|> Jason.decode!() |> Jason.decode!()
end end

@ -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"
}
]

@ -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"
}
]

@ -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

@ -1,7 +1,10 @@
defmodule Explorer.Chain.InternalTransactionTest do defmodule Explorer.Chain.InternalTransactionTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Chain.InternalTransaction alias Explorer.Chain.{InternalTransaction, Wei}
alias Explorer.Factory
import EthereumJSONRPC, only: [integer_to_quantity: 1]
doctest InternalTransaction doctest InternalTransaction
@ -54,4 +57,186 @@ defmodule Explorer.Chain.InternalTransactionTest do
assert Repo.insert(changeset) assert Repo.insert(changeset)
end end
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 end

@ -19,6 +19,7 @@ defmodule Explorer.ExchangeRatesTest do
Application.put_env(:explorer, Explorer.ExchangeRates.Source, source: TestSource) Application.put_env(:explorer, Explorer.ExchangeRates.Source, source: TestSource)
Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates) Application.put_env(:explorer, Explorer.ExchangeRates, table_name: :rates)
Application.put_env(:explorer, Explorer.ExchangeRates, enabled: true)
on_exit(fn -> on_exit(fn ->
Application.put_env(:explorer, Explorer.ExchangeRates.Source, source_configuration) Application.put_env(:explorer, Explorer.ExchangeRates.Source, source_configuration)
@ -135,4 +136,10 @@ defmodule Explorer.ExchangeRatesTest do
assert z == ExchangeRates.lookup("z") assert z == ExchangeRates.lookup("z")
assert nil == ExchangeRates.lookup("nope") assert nil == ExchangeRates.lookup("nope")
end end
test "lookup when disabled" do
Application.put_env(:explorer, Explorer.ExchangeRates, enabled: false)
assert nil == ExchangeRates.lookup("z")
end
end end

@ -21,6 +21,7 @@ defmodule Explorer.KnownTokensTest do
Application.put_env(:explorer, Explorer.KnownTokens.Source, source: TestSource) 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, table_name: :known_tokens)
Application.put_env(:explorer, Explorer.KnownTokens, enabled: true)
on_exit(fn -> on_exit(fn ->
Application.put_env(:explorer, Explorer.KnownTokens.Source, source_configuration) 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 Hash.Address.cast("0x0000000000000000000000000000000000000001") == KnownTokens.lookup("TEST1")
assert nil == KnownTokens.lookup("nope") assert nil == KnownTokens.lookup("nope")
end end
test "lookup when disabled" do
Application.put_env(:explorer, Explorer.KnownTokens, enabled: false)
assert nil == KnownTokens.lookup("z")
end
end end

@ -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

@ -57,6 +57,7 @@ The following async fetchers are launched for importing missing data:
- `token_balance` - `token_balance`
- `token` - `token`
- `contract_code` - `contract_code`
- `staking_pools`
### Async fetchers ### 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_balance`: for `address_token_balances` with null `value_fetched_at`. Also upserts `address_current_token_balances`
- `token`: for `tokens` with `cataloged == false` - `token`: for `tokens` with `cataloged == false`
- `contract_code`: for `transactions` with non-null `created_contract_address_hash` and null `created_contract_code_indexed_at` - `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: Additionally:
- `token_updater` is run every 2 days to update token metadata - `token_updater` is run every 2 days to update token metadata

@ -38,6 +38,7 @@ config :indexer,
# config :indexer, Indexer.Fetcher.ReplacedTransaction.Supervisor, disabled?: true # config :indexer, Indexer.Fetcher.ReplacedTransaction.Supervisor, disabled?: true
# config :indexer, Indexer.Fetcher.BlockReward.Supervisor, disabled?: true # config :indexer, Indexer.Fetcher.BlockReward.Supervisor, disabled?: true
config :indexer, Indexer.Fetcher.StakingPools.Supervisor, disabled?: true
config :indexer, Indexer.Tracer, config :indexer, Indexer.Tracer,
service: :indexer, service: :indexer,

@ -20,6 +20,7 @@ defmodule Indexer.Block.Fetcher do
ContractCode, ContractCode,
InternalTransaction, InternalTransaction,
ReplacedTransaction, ReplacedTransaction,
StakingPools,
Token, Token,
TokenBalance, TokenBalance,
UncleBlock UncleBlock
@ -280,6 +281,10 @@ defmodule Indexer.Block.Fetcher do
def async_import_token_balances(_), do: :ok 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 def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do
UncleBlock.async_fetch_blocks(block_second_degree_relations) UncleBlock.async_fetch_blocks(block_second_degree_relations)
end end

@ -20,7 +20,8 @@ defmodule Indexer.Block.Realtime.Fetcher do
async_import_tokens: 1, async_import_tokens: 1,
async_import_token_balances: 1, async_import_token_balances: 1,
async_import_uncles: 1, async_import_uncles: 1,
fetch_and_import_range: 2 fetch_and_import_range: 2,
async_import_staking_pools: 0
] ]
alias Ecto.Changeset alias Ecto.Changeset
@ -350,6 +351,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
async_import_token_balances(imported) async_import_token_balances(imported)
async_import_uncles(imported) async_import_uncles(imported)
async_import_replaced_transactions(imported) async_import_replaced_transactions(imported)
async_import_staking_pools()
end end
defp balances( defp balances(

@ -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

@ -16,6 +16,7 @@ defmodule Indexer.Supervisor do
InternalTransaction, InternalTransaction,
PendingTransaction, PendingTransaction,
ReplacedTransaction, ReplacedTransaction,
StakingPools,
Token, Token,
TokenBalance, TokenBalance,
TokenUpdater, TokenUpdater,
@ -122,6 +123,7 @@ defmodule Indexer.Supervisor do
{TokenBalance.Supervisor, {TokenBalance.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]},
{StakingPools.Supervisor, [[memory_monitor: memory_monitor]]},
# Out-of-band fetchers # Out-of-band fetchers
{CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]}, {CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]},

@ -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
Loading…
Cancel
Save