diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d5eb06e74..b951086721 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,9 +3,11 @@
### Features
- [#1812](https://github.com/poanetwork/blockscout/pull/1812) - add pagination to addresses page
+- [#1874](https://github.com/poanetwork/blockscout/pull/1874) - add changes to ethereum theme and ethereum logo
- [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x"
- [#1813](https://github.com/poanetwork/blockscout/pull/1813) - add total blocks counter to the main page
- [#1806](https://github.com/poanetwork/blockscout/pull/1806) - verify contracts with a post request
+- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir
- [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces
### Fixes
@@ -17,6 +19,7 @@
- [#1849](https://github.com/poanetwork/blockscout/pull/1849) - Improve chains menu
- [#1869](https://github.com/poanetwork/blockscout/pull/1869) - Fix output and gas extraction in JS tracer for Geth
- [#1868](https://github.com/poanetwork/blockscout/pull/1868) - fix: logs list endpoint performance
+- [#1822](https://github.com/poanetwork/blockscout/pull/1822) - Fix style breaks in decompiled contract code view
### Chore
@@ -199,3 +202,4 @@
- [https://github.com/poanetwork/blockscout/pull/1532](https://github.com/poanetwork/blockscout/pull/1532) - Upgrade elixir to 1.8.1
- [https://github.com/poanetwork/blockscout/pull/1553](https://github.com/poanetwork/blockscout/pull/1553) - Dockerfile: remove 1.7.1 version pin FROM bitwalker/alpine-elixir-phoenix
- [https://github.com/poanetwork/blockscout/pull/1465](https://github.com/poanetwork/blockscout/pull/1465) - Resolve lodash security alert
+
diff --git a/apps/block_scout_web/assets/css/_code.scss b/apps/block_scout_web/assets/css/_code.scss
index f058b69676..b119bcf6d2 100644
--- a/apps/block_scout_web/assets/css/_code.scss
+++ b/apps/block_scout_web/assets/css/_code.scss
@@ -14,7 +14,7 @@ pre {
.pre-decompiled code::before {
content: counter(line);
display: inline-block;
- width: flex;
+ width: 3em;
border-right: 1px solid #ddd;
padding: 0 .5em;
margin-right: .5em;
diff --git a/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss b/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
index 98d2da802c..7437274bf6 100644
--- a/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
@@ -1,3 +1,50 @@
-$primary: #16465b;
-$secondary: #5ab3ff;
-$tertiary: #77a4c5;
+// general
+$primary: #153550;
+$secondary: #49a2ee;
+$tertiary: #4ad7a7;
+$additional-font: #89cae6;
+
+// footer
+$footer-background-color: $primary;
+$footer-title-color: #fff;
+$footer-text-color: #89cae6;
+$footer-item-disc-color: $secondary;
+.footer-logo { filter: brightness(0) invert(1); }
+
+// dashboard
+$dashboard-line-color-price: $tertiary; // price left border
+
+$dashboard-banner-chart-legend-value-color: $additional-font; // chart labels
+
+$dashboard-stats-item-value-color: $additional-font; // stat values
+
+$dashboard-stats-item-border-color: $secondary; // stat border
+
+$dashboard-banner-gradient-start: $primary; // gradient begin
+
+$dashboard-banner-gradient-end: lighten($primary, 5); // gradient end
+
+$dashboard-banner-network-plain-container-background-color: #1c476c; // stats bg
+
+
+// navigation
+.navbar { box-shadow: 0px 0px 30px 0px rgba(21, 53, 80, 0.12); } // header shadow
+$header-icon-border-color-hover: $secondary; // top border on hover
+$header-icon-color-hover: $secondary; // nav icon on hover
+.dropdown-item:hover, .dropdown-item:focus { background-color: $secondary !important; } // dropdown item on hover
+
+// buttons
+$btn-line-bg: #fff; // button bg
+$btn-line-color: $secondary; // button border and font color && hover bg color
+$btn-copy-color: $secondary; // btn copy
+$btn-qr-color: $secondary; // btn qr-code
+
+//links & tile
+.tile a { color: $secondary !important; } // links color for badges
+.tile-type-block {
+ border-left: 4px solid $secondary;
+} // tab active bg
+
+// card
+$card-background-1: $secondary;
+$card-tab-active: $secondary;
diff --git a/apps/block_scout_web/assets/static/images/ethereum_logo.svg b/apps/block_scout_web/assets/static/images/ethereum_logo.svg
index 3f47dc7fe2..b2ebb795f8 100644
--- a/apps/block_scout_web/assets/static/images/ethereum_logo.svg
+++ b/apps/block_scout_web/assets/static/images/ethereum_logo.svg
@@ -1,40 +1 @@
-
-
-
+
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
index 148d82b818..1418255594 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
@@ -18,13 +18,32 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
}
def highlight_decompiled_code(code) do
- @colors
- |> Enum.reduce(code, fn {symbol, rgb}, acc ->
- String.replace(acc, symbol, "")
+ {_, result} =
+ @colors
+ |> Enum.reduce(code, fn {symbol, rgb}, acc ->
+ String.replace(acc, symbol, "")
+ end)
+ |> String.replace("\e[1m", "")
+ |> String.replace("»", "»")
+ |> String.replace("\e[0m", "")
+ |> String.split(~r/\|\<\/span\>/, include_captures: true, trim: true)
+ |> Enum.reduce({"", []}, fn part, {style, acc} ->
+ new_style =
+ cond do
+ String.contains?(part, " part
+ part == "" -> ""
+ true -> style
+ end
+
+ new_part = new_part(part, new_style)
+
+ {new_style, [new_part | acc]}
+ end)
+
+ result
+ |> Enum.reduce("", fn part, acc ->
+ part <> acc
end)
- |> String.replace("\e[1m", "")
- |> String.replace("»", "»")
- |> String.replace("\e[0m", "")
|> add_line_numbers()
end
@@ -41,4 +60,34 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
acc <> "#{line}
\n"
end)
end
+
+ defp new_part(part, new_style) do
+ cond do
+ part == "" ->
+ ""
+
+ part == "" ->
+ ""
+
+ part == new_style ->
+ ""
+
+ new_style == "" ->
+ part
+
+ true ->
+ result =
+ part
+ |> String.split("\n")
+ |> Enum.reduce("", fn p, a ->
+ a <> new_style <> p <> "\n"
+ end)
+
+ if String.ends_with?(part, "\n") do
+ result
+ else
+ String.slice(result, 0..-2)
+ end
+ end
+ end
end
diff --git a/apps/block_scout_web/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