ex_abi/lib/abi/event.ex

140 lines
5.6 KiB
Elixir

defmodule ABI.Event do
@moduledoc """
Tools for decoding event data and topics given a list of function selectors.
"""
alias ABI.FunctionSelector
alias ABI.Util
@type topic :: binary | nil
@type event_value ::
{name :: String.t(), type :: String.t(), indexed? :: boolean, value :: term}
@doc """
Finds the function selector in the ABI, and decodes the event data accordingly.
Ensure that you included events when parsing the ABI, via `include_events?: true`
You'll need to have the data separate from the topics, and pass each topic in
separately. It isn't possible to properly decode the topics + data of an event
without knowing the exact order of the topics, and having those topics
separated from the data itself. That is why this function takes each topic
(even if that topic value is nil) as a separate explicit argument.
The first topic will be the keccak 256 hash of the function signature of the
event. You should not have to calculate this, it should be present as the
first topic of the event.
If any of the topics are dynamic types, you will not be able to get the actual
value of the string. Indexed arguments are placed in topics, and indexed
dynamic types are actually indexed by their keccak 256 hash. The only way for
a contract to provide that value *and* index the argument is to pass the same
value into the event as two separate arguments, one that is indexed and one
that is not. To signify this, those values are returned in a special tuple:
`{:dynamic, value}`.
Examples:
iex> topic1 = ExKeccak.hash_256("WantsPets(string,uint256,bool)")
# first argument is indexed, so it is a topic
...> topic2 = ExKeccak.hash_256("bob")
# third argument is indexed, so it is also a topic
...> topic3 = "0000000000000000000000000000000000000000000000000000000000000001" |> Base.decode16!()
# there are only two indexed arguments, so the fourth topic is `nil`
...> topic4 = nil
# second argument is not, so it is in data
...> data = "0000000000000000000000000000000000000000000000000000000000000000" |> Base.decode16!()
...> File.read!("priv/dog.abi.json")
...> |> Jason.decode!()
...> |> ABI.parse_specification(include_events?: true)
...> |> ABI.Event.find_and_decode(topic1, topic2, topic3, topic4, data)
{%ABI.FunctionSelector{
type: :event,
function: "WantsPets",
input_names: ["_from_human", "_number", "_belly"],
inputs_indexed: [true, false, true],
method_id: <<235, 155, 60, 76, 236, 41, 90, 133, 158, 131, 71, 199, 88, 206, 85, 83, 36, 105, 140, 112, 231, 125, 249, 63, 87, 99, 121, 242, 184, 82, 161, 19>>,
types: [:string, {:uint, 256}, :bool]
},
[
{"_from_human", "string", true, {:dynamic, <<56, 228, 122, 123, 113, 157, 206, 99, 102, 42, 234, 244, 52, 64, 50, 111, 85, 27, 138, 126, 225, 152, 206, 227, 92, 181, 213, 23, 242, 210, 150, 162>>}},
{"_number", "uint256", false, 0},
{"_belly", "bool", true, true}
]
}
"""
@spec find_and_decode([FunctionSelector.t()], topic, topic, topic, topic, binary) ::
{FunctionSelector.t(), [event_value]} | {:error, any}
def find_and_decode(function_selectors, topic1, topic2, topic3, topic4, data) do
input_topics = [topic2, topic3, topic4]
with {:ok, selector} when not is_nil(selector) <-
Util.find_selector_by_event_id(function_selectors, topic1, input_topics) do
args = Enum.zip([selector.input_names, selector.types, selector.inputs_indexed])
{indexed_args, unindexed_args} =
Enum.split_with(args, fn {_name, _type, indexed?} -> indexed? end)
indexed_arg_values = indexed_arg_values(indexed_args, input_topics)
unindexed_arg_types = Enum.map(unindexed_args, &elem(&1, 1))
unindexed_arg_values = ABI.TypeDecoder.decode(data, unindexed_arg_types)
{selector, format_event_values(args, indexed_arg_values, unindexed_arg_values)}
end
end
defp indexed_arg_values(args, topics, acc \\ [])
defp indexed_arg_values([], _, acc), do: Enum.reverse(acc)
defp indexed_arg_values([{_, type, _} | rest_args], [topic | rest_topics], acc) do
value =
if ABI.FunctionSelector.dynamic?(type) do
{bytes, _} = ABI.TypeDecoder.decode_bytes(topic, 32, :left)
# This is explained in the docstring. The caller will almost certainly
# need to know that they don't have an actual encoded value of that type
# but rather they have a 32 bit hash of the value.
{:dynamic, bytes}
else
topic
|> ABI.TypeDecoder.decode([type])
|> List.first()
end
indexed_arg_values(rest_args, rest_topics, [value | acc])
end
defp format_event_values(args, indexed_arg_values, unindexed_arg_values, acc \\ [])
defp format_event_values([], _, _, acc), do: Enum.reverse(acc)
defp format_event_values(
[{name, type, _indexed? = true} | rest_args],
[indexed_arg_value | indexed_args_rest],
unindexed_arg_values,
acc
) do
encoded_type = ABI.FunctionSelector.encode_type(type)
format_event_values(rest_args, indexed_args_rest, unindexed_arg_values, [
{name, encoded_type, true, indexed_arg_value} | acc
])
end
defp format_event_values(
[{name, type, _} | rest_args],
indexed_arg_values,
[unindexed_arg_value | unindexed_args_rest],
acc
) do
encoded_type = ABI.FunctionSelector.encode_type(type)
format_event_values(rest_args, indexed_arg_values, unindexed_args_rest, [
{name, encoded_type, false, unindexed_arg_value} | acc
])
end
end