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