+ <% 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]}]},