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 @@ - - - - -ethereum-logo - - - - - - - - - - - - - + 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 = """ + # + # eveem.org 6 Feb 2019 + # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875 + # + # Let's make the world open source + #  + """ + + assert AddressDecompiledContractView.highlight_decompiled_code(code) == + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n\n" end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index b4a4e7c922..ab725c9c73 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -3,9 +3,10 @@ defmodule EthereumJSONRPC.Geth do Ethereum JSONRPC methods that are only supported by [Geth](https://github.com/ethereum/go-ethereum/wiki/geth). """ - import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, request: 1] + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] - alias EthereumJSONRPC.Geth.Calls + alias EthereumJSONRPC.{FetchedBalance, FetchedCode} + alias EthereumJSONRPC.Geth.{Calls, Tracer} @behaviour EthereumJSONRPC.Variant @@ -28,7 +29,11 @@ defmodule EthereumJSONRPC.Geth do id_to_params |> debug_trace_transaction_requests() |> json_rpc(json_rpc_named_arguments) do - debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + debug_trace_transaction_responses_to_internal_transactions_params( + responses, + id_to_params, + json_rpc_named_arguments + ) end end @@ -62,13 +67,88 @@ defmodule EthereumJSONRPC.Geth do request(%{id: id, method: "debug_traceTransaction", params: [hash_data, %{tracer: @tracer}]}) end - defp debug_trace_transaction_responses_to_internal_transactions_params(responses, id_to_params) + defp debug_trace_transaction_responses_to_internal_transactions_params( + [%{result: %{"structLogs" => _}} | _] = responses, + id_to_params, + json_rpc_named_arguments + ) + when is_map(id_to_params) do + with {:ok, receipts} <- + id_to_params + |> Enum.map(fn {id, %{hash_data: hash_data}} -> + request(%{id: id, method: "eth_getTransactionReceipt", params: [hash_data]}) + end) + |> json_rpc(json_rpc_named_arguments), + {:ok, txs} <- + id_to_params + |> Enum.map(fn {id, %{hash_data: hash_data}} -> + request(%{id: id, method: "eth_getTransactionByHash", params: [hash_data]}) + end) + |> json_rpc(json_rpc_named_arguments) do + receipts_map = Enum.into(receipts, %{}, fn %{id: id, result: receipt} -> {id, receipt} end) + txs_map = Enum.into(txs, %{}, fn %{id: id, result: tx} -> {id, tx} end) + + responses + |> Enum.map(fn %{id: id, result: %{"structLogs" => _} = result} -> + debug_trace_transaction_response_to_internal_transactions_params( + %{id: id, result: Tracer.replay(result, Map.fetch!(receipts_map, id), Map.fetch!(txs_map, id))}, + id_to_params + ) + end) + |> reduce_internal_transactions_params() + |> fetch_missing_data(json_rpc_named_arguments) + end + end + + defp debug_trace_transaction_responses_to_internal_transactions_params( + responses, + id_to_params, + _json_rpc_named_arguments + ) when is_list(responses) and is_map(id_to_params) do responses |> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1, id_to_params)) |> reduce_internal_transactions_params() end + defp fetch_missing_data({:ok, transactions}, json_rpc_named_arguments) when is_list(transactions) do + id_to_params = id_to_params(transactions) + + with {:ok, responses} <- + id_to_params + |> Enum.map(fn + {id, %{created_contract_address_hash: address, block_number: block_number}} -> + FetchedCode.request(%{id: id, block_quantity: integer_to_quantity(block_number), address: address}) + + {id, %{type: "selfdestruct", from: hash_data, block_number: block_number}} -> + FetchedBalance.request(%{id: id, block_quantity: integer_to_quantity(block_number), hash_data: hash_data}) + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + |> json_rpc(json_rpc_named_arguments) do + results = Enum.into(responses, %{}, fn %{id: id, result: result} -> {id, result} end) + + transactions = + id_to_params + |> Enum.map(fn + {id, %{created_contract_address_hash: _} = transaction} -> + %{transaction | created_contract_code: Map.fetch!(results, id)} + + {id, %{type: "selfdestruct"} = transaction} -> + %{transaction | value: Map.fetch!(results, id)} + + {_, transaction} -> + transaction + end) + + {:ok, transactions} + end + end + + defp fetch_missing_data(result, _json_rpc_named_arguments), do: result + defp debug_trace_transaction_response_to_internal_transactions_params(%{id: id, result: calls}, id_to_params) when is_map(id_to_params) do %{block_number: block_number, hash_data: transaction_hash, transaction_index: transaction_index} = diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex index 88a32c704f..434cc7b34d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex @@ -328,9 +328,8 @@ defmodule EthereumJSONRPC.Geth.Call do "from" => from_address_hash, "to" => to_address_hash, "gas" => gas, - "gasUsed" => gas_used, "input" => input, - "output" => output, + "error" => error, "value" => value }) when call_type in ~w(call callcode delegatecall) do @@ -345,9 +344,8 @@ defmodule EthereumJSONRPC.Geth.Call do from_address_hash: from_address_hash, to_address_hash: to_address_hash, gas: gas, - gas_used: gas_used, input: input, - output: output, + error: error, value: value } end @@ -363,8 +361,9 @@ defmodule EthereumJSONRPC.Geth.Call do "from" => from_address_hash, "to" => to_address_hash, "gas" => gas, + "gasUsed" => gas_used, "input" => input, - "error" => error, + "output" => output, "value" => value }) when call_type in ~w(call callcode delegatecall) do @@ -379,8 +378,9 @@ defmodule EthereumJSONRPC.Geth.Call do from_address_hash: from_address_hash, to_address_hash: to_address_hash, gas: gas, + gas_used: gas_used, input: input, - error: error, + output: output, value: value } end @@ -425,13 +425,11 @@ defmodule EthereumJSONRPC.Geth.Call do "transactionHash" => transaction_hash, "index" => index, "traceAddress" => trace_address, - "type" => "create", + "type" => "create" = type, "from" => from_address_hash, - "createdContractAddressHash" => created_contract_address_hash, + "error" => error, "gas" => gas, - "gasUsed" => gas_used, "init" => init, - "createdContractCode" => created_contract_code, "value" => value }) do %{ @@ -440,13 +438,11 @@ defmodule EthereumJSONRPC.Geth.Call do transaction_hash: transaction_hash, index: index, trace_address: trace_address, - type: "create", + type: type, from_address_hash: from_address_hash, gas: gas, - gas_used: gas_used, - created_contract_address_hash: created_contract_address_hash, + error: error, init: init, - created_contract_code: created_contract_code, value: value } end @@ -457,11 +453,13 @@ defmodule EthereumJSONRPC.Geth.Call do "transactionHash" => transaction_hash, "index" => index, "traceAddress" => trace_address, - "type" => "create" = type, + "type" => "create", "from" => from_address_hash, - "error" => error, + "createdContractAddressHash" => created_contract_address_hash, "gas" => gas, + "gasUsed" => gas_used, "init" => init, + "createdContractCode" => created_contract_code, "value" => value }) do %{ @@ -470,11 +468,13 @@ defmodule EthereumJSONRPC.Geth.Call do transaction_hash: transaction_hash, index: index, trace_address: trace_address, - type: type, + type: "create", from_address_hash: from_address_hash, gas: gas, - error: error, + gas_used: gas_used, + created_contract_address_hash: created_contract_address_hash, init: init, + created_contract_code: created_contract_code, value: value } end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex new file mode 100644 index 0000000000..6545250b4a --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/tracer.ex @@ -0,0 +1,282 @@ +defmodule EthereumJSONRPC.Geth.Tracer do + @moduledoc """ + Elixir implementation of a custom tracer (`priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js`) + for variants that don't support specifying tracer in [debug_traceTransaction](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction) calls. + """ + + import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] + + def replay(%{"structLogs" => logs} = result, receipt, tx) when is_list(logs) do + %{"contractAddress" => contract_address} = receipt + %{"from" => from, "to" => to, "value" => value, "input" => input} = tx + + top = + to + |> if do + %{ + "type" => "call", + "callType" => "call", + "to" => to, + "input" => input, + "output" => Map.get(result, "return", "0x" <> Map.get(result, "returnValue", "")) + } + else + %{ + "type" => "create", + "init" => input, + "createdContractAddressHash" => contract_address, + "createdContractCode" => "0x" + } + end + |> Map.merge(%{ + "from" => from, + "traceAddress" => [], + "value" => value, + "gas" => 0, + "gasUsed" => 0 + }) + + ctx = %{ + depth: 1, + stack: [top], + trace_address: [0], + calls: [[]] + } + + logs + |> Enum.reduce(ctx, &step/2) + |> finalize() + end + + defp step(%{"error" => _}, %{stack: [%{"error" => _} | _]} = ctx), do: ctx + + defp step( + %{"error" => _} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) do + call = process_return(log, Map.put(call, "error", "error")) + + subsubcalls = + subsubcalls + |> Enum.reverse() + |> Enum.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + + %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + } + end + + defp step( + %{"depth" => log_depth} = log, + %{ + depth: stack_depth, + stack: [call | stack], + trace_address: [_, trace_index | trace_address], + calls: [subsubcalls, subcalls | calls] + } = ctx + ) + when log_depth == stack_depth - 1 do + call = process_return(log, call) + + subsubcalls = + subsubcalls + |> Enum.reverse() + |> Enum.map(fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => call["createdContractAddressHash"] || call["to"]} + end) + + step(log, %{ + ctx + | depth: stack_depth - 1, + stack: stack, + trace_address: [trace_index + 1 | trace_address], + calls: [[subsubcalls, call | subcalls] | calls] + }) + end + + defp step(%{"gas" => log_gas, "gasCost" => log_gas_cost} = log, %{stack: [%{"gas" => call_gas} = call | stack]} = ctx) do + gas = max(call_gas, log_gas) + op(log, %{ctx | stack: [%{call | "gas" => gas, "gasUsed" => gas - log_gas - log_gas_cost} | stack]}) + end + + defp op(%{"op" => "CREATE"} = log, ctx), do: create_op(log, ctx) + defp op(%{"op" => "SELFDESTRUCT"} = log, ctx), do: self_destruct_op(log, ctx) + defp op(%{"op" => "CALL"} = log, ctx), do: call_op(log, "call", ctx) + defp op(%{"op" => "CALLCODE"} = log, ctx), do: call_op(log, "callcode", ctx) + defp op(%{"op" => "DELEGATECALL"} = log, ctx), do: call_op(log, "delegatecall", ctx) + defp op(%{"op" => "STATICCALL"} = log, ctx), do: call_op(log, "staticcall", ctx) + defp op(%{"op" => "REVERT"}, ctx), do: revert_op(ctx) + defp op(_, ctx), do: ctx + + defp process_return(%{"stack" => log_stack}, %{"type" => "create"} = call) do + [ret | _] = Enum.reverse(log_stack) + + case quantity_to_integer(ret) do + 0 -> Map.put(call, "error", call["error"] || "internal failure") + _ -> %{call | "createdContractAddressHash" => "0x" <> String.slice(ret, 24, 40)} + end + end + + defp process_return( + %{"stack" => log_stack, "memory" => log_memory}, + %{"outputOffset" => out_off, "outputLength" => out_len} = call + ) do + [ret | _] = Enum.reverse(log_stack) + + ret + |> quantity_to_integer() + |> case do + 0 -> + Map.put(call, "error", call["error"] || "internal failure") + + _ -> + output = + log_memory + |> IO.iodata_to_binary() + |> String.slice(out_off, out_len) + + %{call | "output" => "0x" <> output} + end + |> Map.drop(["outputOffset", "outputLength"]) + end + + defp create_op( + %{"stack" => log_stack, "memory" => log_memory}, + %{depth: stack_depth, stack: stack, trace_address: trace_address, calls: calls} = ctx + ) do + [value, input_offset, input_length | _] = Enum.reverse(log_stack) + + init = + log_memory + |> IO.iodata_to_binary() + |> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) + + call = %{ + "type" => "create", + "from" => nil, + "traceAddress" => Enum.reverse(trace_address), + "init" => "0x" <> init, + "gas" => 0, + "gasUsed" => 0, + "value" => "0x" <> value, + "createdContractAddressHash" => nil, + "createdContractCode" => "0x" + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call | stack], + trace_address: [0 | trace_address], + calls: [[] | calls] + } + end + + defp self_destruct_op( + %{"stack" => log_stack, "gas" => log_gas, "gasCost" => log_gas_cost}, + %{trace_address: [trace_index | trace_address], calls: [subcalls | calls]} = ctx + ) do + [to | _] = Enum.reverse(log_stack) + + if quantity_to_integer(to) in 1..8 do + ctx + else + call = %{ + "type" => "selfdestruct", + "from" => nil, + "to" => "0x" <> String.slice(to, 24, 40), + "traceAddress" => Enum.reverse([trace_index | trace_address]), + "gas" => log_gas, + "gasUsed" => log_gas_cost, + "value" => "0x0" + } + + %{ctx | trace_address: [trace_index + 1 | trace_address], calls: [[call | subcalls] | calls]} + end + end + + defp call_op( + %{"stack" => log_stack, "memory" => log_memory}, + call_type, + %{ + depth: stack_depth, + stack: [%{"value" => parent_value} = parent | stack], + trace_address: trace_address, + calls: calls + } = ctx + ) do + [_, to | log_stack] = Enum.reverse(log_stack) + + {value, [input_offset, input_length, output_offset, output_length | _]} = + case call_type do + "delegatecall" -> + {parent_value, log_stack} + + "staticcall" -> + {"0x0", log_stack} + + _ -> + [value | rest] = log_stack + {"0x" <> value, rest} + end + + input = + log_memory + |> IO.iodata_to_binary() + |> String.slice(quantity_to_integer("0x" <> input_offset) * 2, quantity_to_integer("0x" <> input_length) * 2) + + call = %{ + "type" => "call", + "callType" => call_type, + "from" => nil, + "to" => "0x" <> String.slice(to, 24, 40), + "traceAddress" => Enum.reverse(trace_address), + "input" => "0x" <> input, + "output" => "0x", + "outputOffset" => quantity_to_integer("0x" <> output_offset) * 2, + "outputLength" => quantity_to_integer("0x" <> output_length) * 2, + "gas" => 0, + "gasUsed" => 0, + "value" => value + } + + %{ + ctx + | depth: stack_depth + 1, + stack: [call, parent | stack], + trace_address: [0 | trace_address], + calls: [[] | calls] + } + end + + defp revert_op(%{stack: [last | stack]} = ctx) do + %{ctx | stack: [Map.put(last, "error", "execution reverted") | stack]} + end + + defp finalize(%{stack: [top], calls: [calls]}) do + calls = + Enum.map(calls, fn + subcalls when is_list(subcalls) -> subcalls + subcall when is_map(subcall) -> %{subcall | "from" => top["createdContractAddressHash"] || top["to"]} + end) + + [top | Enum.reverse(calls)] + |> List.flatten() + |> Enum.map(fn %{"gas" => gas, "gasUsed" => gas_used} = call -> + %{call | "gas" => integer_to_quantity(gas), "gasUsed" => integer_to_quantity(gas_used)} + end) + end +end