Skip to content

Commit

Permalink
Merge pull request #1859 from poanetwork/store-raw-transaction-trace
Browse files Browse the repository at this point in the history
feat: show raw transaction traces
  • Loading branch information
vbaranov committed May 1, 2019
2 parents 95f50ce + 1d22acd commit e998306
Show file tree
Hide file tree
Showing 15 changed files with 506 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x"
- [#1813](https://github.com/poanetwork/blockscout/pull/1813) - add total blocks counter to the main page
- [#1806](https://github.com/poanetwork/blockscout/pull/1806) - verify contracts with a post request
- [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces

### Fixes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import $ from 'jquery'
import hljs from 'highlight.js'

// only activate highlighting on pages with this selector
if ($('[data-activate-highlight]').length > 0) {
hljs.initHighlightingOnLoad()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule BlockScoutWeb.TransactionRawTraceController do
use BlockScoutWeb, :controller

alias BlockScoutWeb.TransactionView
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token

def index(conn, %{"transaction_id" => hash_string}) do
with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string),
{:ok, transaction} <-
Chain.hash_to_transaction(
hash,
necessity_by_association: %{
:block => :optional,
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
[to_address: :smart_contract] => :optional,
:token_transfers => :optional
}
) do
options = [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional
}
]

internal_transactions = Chain.transaction_to_internal_transactions(transaction, options)

render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
internal_transactions: internal_transactions,
block_height: Chain.block_height(),
show_token_transfers: Chain.transaction_has_token_transfers?(hash),
transaction: transaction
)
else
:error ->
conn
|> put_status(422)
|> put_view(TransactionView)
|> render("invalid.html", transaction_hash: hash_string)

{:error, :not_found} ->
conn
|> put_status(404)
|> put_view(TransactionView)
|> render("not_found.html", transaction_hash: hash_string)
end
end
end
7 changes: 7 additions & 0 deletions apps/block_scout_web/lib/block_scout_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ defmodule BlockScoutWeb.Router do
as: :internal_transaction
)

resources(
"/raw_trace",
TransactionRawTraceController,
only: [:index],
as: :raw_trace
)

resources("/logs", TransactionLogController, only: [:index], as: :log)

resources("/token_transfers", TransactionTokenTransferController,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@
"data-test": "transaction_logs_link"
)
%>
<%= link(
gettext("Raw Trace"),
class: "nav-link #{tab_status("raw_trace", @conn.request_path)}",
to: transaction_raw_trace_path(@conn, :index, @transaction)
) %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render BlockScoutWeb.TransactionView, "_metatags.html", conn: @conn, transaction: @transaction %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<section class="container">
<%= render BlockScoutWeb.TransactionView, "overview.html", assigns %>

<div class="card">
<div class="card-header">
<%= render BlockScoutWeb.TransactionView, "_tabs.html", assigns %>
</div>

<div class="card-body">
<h2 class="card-title"><%= gettext "Raw Trace" %></h2>
<%= if Enum.count(@internal_transactions) > 0 do %>
<pre class="pre-scrollable line-numbers" data-activate-highlight><code class="json "><%= for {line, number} <- raw_traces_with_lines(@internal_transactions) do %><div data-line-number="<%= number %>"><%= line %></div><% end %></code></pre>
<% else %>
No trace entries found.
<% end %>
</div>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apps/block_scout_web/priv/gettext/default.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
7 changes: 7 additions & 0 deletions apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
119 changes: 118 additions & 1 deletion apps/explorer/lib/explorer/chain/internal_transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
42 changes: 42 additions & 0 deletions apps/explorer/lib/explorer/chain/internal_transaction/action.ex
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions apps/explorer/lib/explorer/chain/internal_transaction/result.ex
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit e998306

Please sign in to comment.