From 21a1c1403c81c7459d1cd2f2950758c486e67af1 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Mon, 19 Nov 2018 15:14:14 -0500 Subject: [PATCH] feat: fix ABI decodding, and decode tx logs --- .../css/components/_transaction-input.scss | 25 ++- .../templates/transaction/overview.html.eex | 155 ++++++++---------- .../templates/transaction_log/index.html.eex | 116 ++++++++++--- .../views/abi_encoded_value_view.ex | 108 ++++++++++++ .../views/transaction_log_view.ex | 6 + .../views/abi_encoded_value_view_test.exs | 91 ++++++++++ apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/chain/log.ex | 59 +++++++ mix.lock | 2 +- 9 files changed, 442 insertions(+), 122 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs diff --git a/apps/block_scout_web/assets/css/components/_transaction-input.scss b/apps/block_scout_web/assets/css/components/_transaction-input.scss index 63c3a847b8..12ca615be8 100644 --- a/apps/block_scout_web/assets/css/components/_transaction-input.scss +++ b/apps/block_scout_web/assets/css/components/_transaction-input.scss @@ -1,9 +1,26 @@ .raw-transaction-input{ - display: none; + display: none; +} + +.raw-transaction-log-topics{ + display: none; +} + +.raw-transaction-log-data{ + display: none; } .transaction-input-text{ - resize: vertical; - overflow: auto; - word-break: break-all; + white-space: pre; + color: black; + + pre{ + code{ + color: black; + } + } +} + +.transaction-input-table{ + overflow-x: scroll; } diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index 21440d0c8b..23008b60c8 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -75,102 +75,75 @@ -
- <%= case decoded_input_data(@transaction) do %> - <% {:error, :contract_not_verified} -> %> -
<%= gettext "Input" %>
-
-
- To see decoded input data, the contract must be verified. -
-
- <% {:error, :could_not_decode} -> %> -
<%= gettext "Input" %>
-
-
- Failed to decode input data. Some dynamic types are not currently supported. -
-
- <% {:ok, method_id, text, mapping} -> %> -
<%= gettext "Input" %>
-
- - - - - +

<%= gettext "Input" %>

+ <%= case decoded_input_data(@transaction) do %> + <% {:error, :contract_not_verified} -> %> +
+ To see decoded input data, the contract must be verified. +
+ <% {:error, :could_not_decode} -> %> +
+ Failed to decode input data. Some dynamic types are not currently supported. +
+ <% {:ok, method_id, text, mapping} -> %> +
Method Id0x<%= method_id %>
+ + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ + + + + + + + + <%= for {name, type, value} <- mapping do %> - - + + + + -
<%= gettext "Name" %><%= gettext "Type" %><%= gettext "Data" %>
Call<%= text %> + <% copy_text = BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) %> + + <%= name %><%= type %> +
<%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %>
+
- - - - - - - - - <%= for {{name, type, value}, index} <- Enum.with_index(mapping) do %> - - - - - <%= case type do %> - <% "address" -> %> - <% address = "0x" <> Base.encode16(value, case: :lower) %> - - + <% end %> +
#NameTypeData
<%= index %><%= name %><%= type %> - - - -
+ <% _ -> %> + <%= nil %> + <% end %> - <% _ -> %> - -
<%= value %> - - - - - <% end %> - - <% end %> - -
- <% _ -> %> - <%= nil %> - <% end %> + <%= unless @transaction.input.bytes in [<<>>, nil] do %> +

<%= gettext "Raw Input" %>

+
+ +
+
+ + - <%= unless @transaction.input.bytes in [<<>>, nil] do %> -
<%= gettext "Raw Input" %>
-
-
- +
+
+                  <%= @transaction.input %>
+                
-
- -
- -
-                    <%= @transaction.input %>
-                  
-
-
-
- <% end %> -
+ + <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex index 4eb501a5fd..7f8d7066c4 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex @@ -23,39 +23,105 @@ ) %> -
<%= gettext "Topics" %>
+
<%= gettext "Decoded" %>
- <%= unless is_nil(log.first_topic) do %> -
- [0] - <%= log.first_topic %> -
- <% end %> - <%= unless is_nil(log.second_topic) do %> -
- [1] - <%= log.second_topic %> -
- <% end %> - <%= unless is_nil(log.third_topic) do %> -
- [2] - <%= log.third_topic %> -
- <% end %> - <%= unless is_nil(log.fourth_topic) do %> -
- [3] - <%= log.fourth_topic %> -
+ <%= case decode(log, @transaction) do %> + <% {:error, :contract_not_verified} -> %> +
+ To see decoded input data, the contract must be verified. +
+ <% {:error, :could_not_decode} -> %> +
+ Failed to decode log data. +
+ <% {:ok, method_id, text, mapping} -> %> + + + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ + + + + + + + + <%= for {name, type, indexed?, value} <- mapping do %> + + + + + + + + <% end %> +
<%= gettext "Name" %><%= gettext "Type" %><%= gettext "Indexed?" %><%= gettext "Data" %>
+ <% copy_text = BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) %> + + + <%= name %><%= type %><%= indexed? %> +
<%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %>
+
<% end %> + +
+
<%= gettext "Topics" %>
+
+
+ +
+
+ + <%= unless is_nil(log.first_topic) do %> +
+ [0] + <%= log.first_topic %> +
+ <% end %> + <%= unless is_nil(log.second_topic) do %> +
+ [1] + <%= log.second_topic %> +
+ <% end %> + <%= unless is_nil(log.third_topic) do %> +
+ [2] + <%= log.third_topic %> +
+ <% end %> + <%= unless is_nil(log.fourth_topic) do %> +
+ [3] + <%= log.fourth_topic %> +
+ <% end %> +
<%= gettext "Data" %>
<%= unless is_nil(log.data) do %> -
+
+ +
+ +
+ <%= log.data %>
<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex new file mode 100644 index 0000000000..a0b5e2bd61 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex @@ -0,0 +1,108 @@ +defmodule BlockScoutWeb.ABIEncodedValueView do + @moduledoc """ + Renders a decoded value that is encoded according to an ABI. + + Does not leverage an eex template because it renders formatted + values via `
` tags, and that is hard to do in an eex template.
+  """
+  use BlockScoutWeb, :view
+
+  require Logger
+
+  def value_html(type, value) do
+    decoded_type = ABI.FunctionSelector.decode_type(type)
+
+    do_value_html(decoded_type, value)
+  rescue
+    exception ->
+      Logger.warn(fn ->
+        ["Error determining value html for #{inspect(type)}: ", Exception.format(:error, exception)]
+      end)
+  end
+
+  def copy_text(type, value) do
+    decoded_type = ABI.FunctionSelector.decode_type(type)
+
+    do_copy_text(decoded_type, value)
+  rescue
+    exception ->
+      Logger.warn(fn ->
+        ["Error determining copy text for #{inspect(type)}: ", Exception.format(:error, exception)]
+      end)
+  end
+
+  def do_copy_text({:bytes, _type}, value) do
+    hex(value)
+  end
+
+  def do_copy_text({:array, type, _}, value) do
+    do_copy_text({:array, type}, value)
+  end
+
+  def do_copy_text({:array, type}, value) do
+    values =
+      value
+      |> Enum.map(&do_copy_text(type, &1))
+      |> Enum.intersperse(", ")
+
+    ~E|[<%= values %>]|
+  end
+
+  def do_copy_text(_, {:dynamic, value}) do
+    hex(value)
+  end
+
+  def do_copy_text(type, value) when type in [:bytes, :address] do
+    hex(value)
+  end
+
+  def do_copy_text(_type, value) do
+    to_string(value)
+  end
+
+  defp do_value_html(type, value, depth \\ 0)
+
+  defp do_value_html({:bytes, _}, value, depth) do
+    do_value_html(:bytes, value, depth)
+  end
+
+  defp do_value_html({:array, type, _}, value, depth) do
+    do_value_html({:array, type}, value, depth)
+  end
+
+  defp do_value_html({:array, type}, value, depth) do
+    values =
+      Enum.map(value, fn inner_value ->
+        do_value_html(type, inner_value, depth + 1)
+      end)
+
+    spacing = String.duplicate(" ", depth * 2)
+    delimited = Enum.intersperse(values, ",\n")
+
+    ~E|<%= spacing %>[<%= "\n" %><%= delimited %><%= "\n" %><%= spacing %>]|
+  end
+
+  defp do_value_html(type, value, depth) do
+    spacing = String.duplicate(" ", depth * 2)
+    ~E|<%= spacing %><%=base_value_html(type, value)%>|
+    [spacing, base_value_html(type, value)]
+  end
+
+  def base_value_html(_, {:dynamic, value}) do
+    hex(value)
+  end
+
+  def base_value_html(:address, value) do
+    address = hex(value)
+
+    ~E|<%= address %>|
+  end
+
+  def base_value_html(:bytes, value) do
+    hex(value)
+  end
+
+  def base_value_html(_, value), do: Phoenix.HTML.html_escape(value)
+
+  defp hex(value), do: "0x" <> Base.encode16(value, case: :lower)
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex
index 96cf62fe9a..50d406433c 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_log_view.ex
@@ -1,4 +1,10 @@
 defmodule BlockScoutWeb.TransactionLogView do
   use BlockScoutWeb, :view
   @dialyzer :no_match
+
+  alias Explorer.Chain.Log
+
+  def decode(log, transaction) do
+    Log.decode(log, transaction)
+  end
 end
diff --git a/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs
new file mode 100644
index 0000000000..a63f15cf64
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/views/abi_encoded_value_view_test.exs
@@ -0,0 +1,91 @@
+defmodule BlockScoutWeb.ABIEncodedValueViewTest do
+  use BlockScoutWeb.ConnCase, async: true
+
+  alias BlockScoutWeb.ABIEncodedValueView
+
+  defp value_html(type, value) do
+    type
+    |> ABIEncodedValueView.value_html(value)
+    |> Phoenix.HTML.Safe.to_iodata()
+    |> IO.iodata_to_binary()
+  end
+
+  defp copy_text(type, value) do
+    type
+    |> ABIEncodedValueView.copy_text(value)
+    |> Phoenix.HTML.Safe.to_iodata()
+    |> IO.iodata_to_binary()
+  end
+
+  describe "value_html/2" do
+    test "it formats addresses as links" do
+      address = "0x0000000000000000000000000000000000000000"
+      address_bytes = address |> String.trim_leading("0x") |> Base.decode16!()
+
+      expected = ~s(#{address})
+
+      assert value_html("address", address_bytes) == expected
+    end
+
+    test "it formats lists with newlines and spaces" do
+      expected = String.trim("""
+      [
+        1,
+        2,
+        3,
+        4
+      ]
+      """)
+
+      assert value_html("uint[]", [1, 2, 3, 4]) == expected
+    end
+
+    test "it formats nested lists with nested depth" do
+      expected = String.trim("""
+      [
+        [
+          1,
+          2
+        ],
+        [
+          3,
+          4
+        ]
+      ]
+      """)
+
+      assert value_html("uint[][]", [[1, 2], [3, 4]]) == expected
+    end
+
+    test "it formats lists of addresses as a list of links" do
+      address = "0x0000000000000000000000000000000000000000"
+      address_link = ~s(#{address})
+
+      expected = String.trim("""
+      [
+        #{address_link},
+        #{address_link},
+        #{address_link},
+        #{address_link}
+      ]
+      """)
+
+      address_bytes = "0x0000000000000000000000000000000000000000" |> String.trim_leading("0x") |> Base.decode16!()
+
+      assert value_html("address[]", [address_bytes, address_bytes, address_bytes, address_bytes]) == expected
+    end
+  end
+
+  describe "copy_text/2" do
+    test "it skips link formatting of addresses" do
+      address = "0x0000000000000000000000000000000000000000"
+      address_bytes = address |> String.trim_leading("0x") |> Base.decode16!()
+
+      assert copy_text("address", address_bytes) == address
+    end
+
+    test "it skips the formatting when copying lists" do
+      assert copy_text("uint[]", [1, 2, 3, 4]) == "[1, 2, 3, 4]"
+    end
+  end
+end
diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs
index 16608e0942..02c8ccfe53 100644
--- a/apps/ethereum_jsonrpc/mix.exs
+++ b/apps/ethereum_jsonrpc/mix.exs
@@ -79,7 +79,7 @@ defmodule EthereumJsonrpc.MixProject do
       # Convert unix timestamps in JSONRPC to DateTimes
       {:timex, "~> 3.4"},
       # Encode/decode function names and arguments
-      {:ex_abi, "~> 0.1.17"},
+      {:ex_abi, "~> 0.1.18"},
       # `:verify_fun` for `Socket.Web.connect`
       {:ssl_verify_fun, "~> 1.1"},
       # `EthereumJSONRPC.WebSocket`
diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex
index 9dcb31405a..f97a24b378 100644
--- a/apps/explorer/lib/explorer/chain/log.ex
+++ b/apps/explorer/lib/explorer/chain/log.ex
@@ -3,6 +3,8 @@ defmodule Explorer.Chain.Log do
 
   use Explorer.Schema
 
+  require Logger
+
   alias Explorer.Chain.{Address, Data, Hash, Transaction}
 
   @required_attrs ~w(address_hash data index transaction_hash)a
@@ -98,4 +100,61 @@ defmodule Explorer.Chain.Log do
     |> cast(attrs, @optional_attrs)
     |> validate_required(@required_attrs)
   end
+
+  @doc """
+  Decode transaction log data.
+  """
+  def decode(log, transaction) do
+    abi = transaction.to_address.smart_contract.abi
+
+    with {:ok, selector, mapping} <- find_and_decode(abi, log),
+         identifier <- Base.encode16(selector.method_id, case: :lower),
+         text <- function_call(selector.function, mapping),
+         do: {:ok, identifier, text, mapping}
+  end
+
+  defp find_and_decode(abi, log) do
+    with {selector, mapping} <-
+           abi
+           |> ABI.parse_specification(include_events?: true)
+           |> ABI.Event.find_and_decode(
+             decode16!(log.first_topic),
+             decode16!(log.second_topic),
+             decode16!(log.third_topic),
+             decode16!(log.fourth_topic),
+             log.data.bytes
+           ) do
+      {:ok, selector, mapping}
+    end
+  rescue
+    _ ->
+      Logger.warn(fn -> ["Could not decode input data for log: ", Hash.to_iodata(log.hash)] end)
+      {:error, :could_not_decode}
+  end
+
+  defp function_call(name, mapping) do
+    text =
+      mapping
+      |> Stream.map(fn {name, type, indexed?, _value} ->
+        indexed_keyword =
+          if indexed? do
+            ["indexed "]
+          else
+            []
+          end
+
+        [type, " ", indexed_keyword, name]
+      end)
+      |> Enum.intersperse(", ")
+
+    IO.iodata_to_binary([name, "(", text, ")"])
+  end
+
+  def decode16!(nil), do: nil
+
+  def decode16!(value) do
+    value
+    |> String.trim_leading("0x")
+    |> Base.decode16!(case: :lower)
+  end
 end
diff --git a/mix.lock b/mix.lock
index a1d0a1c603..3a6bf7bd26 100644
--- a/mix.lock
+++ b/mix.lock
@@ -28,7 +28,7 @@
   "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
   "ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
   "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
-  "ex_abi": {:hex, :ex_abi, "0.1.17", "11822f88b3ed70773c64858a49321b3c51ed913128a3f9fc7a05fa7ceb19d8fa", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"},
+  "ex_abi": {:hex, :ex_abi, "0.1.18", "19db9bffdd201edbdff97d7dd5849291218b17beda045c1b76bff5248964f37d", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_cldr": {:hex, :ex_cldr, "1.3.2", "8f4a00c99d1c537b8e8db7e7903f4bd78d82a7289502d080f70365392b13921b", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, optional: true]}]},
   "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.2.0", "ef27299922da913ffad1ed296cacf28b6452fc1243b77301dc17c03276c6ee34", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, optional: false]}, {:ex_cldr, "~> 1.3", [hex: :ex_cldr, optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, optional: false]}]},
   "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, optional: false]}]},