Skip to content

Commit

Permalink
Merge pull request #11 from zachdaniel/add-event-parsing
Browse files Browse the repository at this point in the history
Add event parsing
  • Loading branch information
ayrat555 authored Nov 17, 2018
2 parents 5456575 + 84f906b commit 054d433
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 35 deletions.
61 changes: 31 additions & 30 deletions lib/abi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule ABI do
it to or from types that Solidity understands.
"""

alias ABI.Util

@doc """
Encodes the given data into the function signature or tuple signature.
Expand Down Expand Up @@ -87,18 +89,20 @@ defmodule ABI do
otherwise this won't work as expected. If you are decoding transaction input data
the identifier is the first four bytes and should already be there.
To find and decode events instead of functions, see `find_and_decode_event/6`
## Examples
iex> File.read!("priv/dog.abi.json")
...> |> Poison.decode!
...> |> ABI.parse_specification
...> |> ABI.find_and_decode("b85d0bd200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" |> Base.decode16!(case: :lower))
{%ABI.FunctionSelector{function: "bark", input_names: ["at", "loudly"], method_id: <<184, 93, 11, 210>>, returns: [], types: [:address, :bool]}, [<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, true]}
{%ABI.FunctionSelector{type: :function, function: "bark", input_names: ["at", "loudly"], method_id: <<184, 93, 11, 210>>, returns: [], types: [:address, :bool]}, [<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>, true]}
"""
def find_and_decode(function_selectors, data) do
with {:ok, method_id, rest} <- split_method_id(data),
with {:ok, method_id, rest} <- Util.split_method_id(data),
{:ok, selector} when not is_nil(selector) <-
find_selector_by_method_id(function_selectors, method_id) do
Util.find_selector_by_method_id(function_selectors, method_id) do
{selector, decode(selector, rest)}
end
end
Expand All @@ -110,13 +114,18 @@ defmodule ABI do
This function can be used in combination with a JSON parser, e.g. [`Poison`](https://hex.pm/packages/poison), to parse ABI specification JSON files.
Opts:
* `:include_events?` - Include events in the output list (as `%FunctionSelector{}` with a type of `:event`). Defaults to `false`
for backwards compatibility reasons.
## Examples
iex> File.read!("priv/dog.abi.json")
...> |> Poison.decode!
...> |> ABI.parse_specification
[%ABI.FunctionSelector{function: "bark", input_names: ["at", "loudly"], method_id: <<184, 93, 11, 210>>, returns: [], types: [:address, :bool]},
%ABI.FunctionSelector{function: "rollover", method_id: <<176, 86, 180, 154>>, returns: [:bool], types: []}]
[%ABI.FunctionSelector{type: :function, function: "bark", input_names: ["at", "loudly"], method_id: <<184, 93, 11, 210>>, returns: [], types: [:address, :bool]},
%ABI.FunctionSelector{type: :function, function: "rollover", method_id: <<176, 86, 180, 154>>, returns: [:bool], types: []}]
iex> [%{
...> "constant" => true,
Expand All @@ -131,7 +140,7 @@ defmodule ABI do
...> "type" => "function"
...> }]
...> |> ABI.parse_specification
[%ABI.FunctionSelector{function: "bark", method_id: <<184, 93, 11, 210>>, input_names: ["at", "loudly"], returns: [], types: [:address, :bool]}]
[%ABI.FunctionSelector{type: :function, function: "bark", method_id: <<184, 93, 11, 210>>, input_names: ["at", "loudly"], returns: [], types: [:address, :bool]}]
iex> [%{
...> "inputs" => [
Expand All @@ -150,32 +159,24 @@ defmodule ABI do
...> "type" => "fallback"
...> }]
...> |> ABI.parse_specification
[%ABI.FunctionSelector{function: nil, returns: [], types: [], method_id: nil}]
"""
def parse_specification(doc) do
doc
|> Enum.map(&ABI.FunctionSelector.parse_specification_item/1)
|> Enum.reject(&is_nil/1)
end

defp find_selector_by_method_id(function_selectors, method_id_target) do
function_selector =
Enum.find(function_selectors, fn %{method_id: method_id} ->
method_id == method_id_target
end)
[%ABI.FunctionSelector{type: :function, function: nil, returns: [], types: [], method_id: nil}]
if function_selector do
{:ok, function_selector}
iex> File.read!("priv/dog.abi.json")
...> |> Poison.decode!
...> |> ABI.parse_specification(include_events?: true)
...> |> Enum.filter(&(&1.type == :event))
[%ABI.FunctionSelector{type: :event, function: "WantsPets", input_names: ["_from_human", "_number", "_belly"], inputs_indexed: [true, false, true], method_id: <<235, 155, 60, 76>>, types: [:string, {:uint, 256}, :bool]}]
"""
def parse_specification(doc, opts \\ []) do
if opts[:include_events?] do
doc
|> Enum.map(&ABI.FunctionSelector.parse_specification_item/1)
|> Enum.reject(&is_nil/1)
else
{:error, :no_matching_function}
doc
|> Enum.map(&ABI.FunctionSelector.parse_specification_item/1)
|> Enum.reject(&is_nil/1)
|> Enum.reject(&(&1.type == :event))
end
end

defp split_method_id(<<method_id::binary-size(4), rest::binary>>) do
{:ok, method_id, rest}
end

defp split_method_id(_) do
{:error, :invalid_data}
end
end
140 changes: 140 additions & 0 deletions lib/abi/event.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
defmodule ABI.Event do
@moduledoc """
Tools for decoding event data and topics given a list of function selectors.
"""

alias ABI.Util
alias ABI.FunctionSelector

@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 = :keccakf1600.hash(:sha3_256, "WantsPets(string,uint256,bool)")
# first argument is indexed, so it is a topic
...> topic2 = :keccakf1600.hash(:sha3_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")
...> |> Poison.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>>,
types: [:string, {:uint, 256}, :bool]
},
[
{"_from_human", "string", true, {:dynamic, :keccakf1600.hash(:sha3_256, "bob")}},
{"_number", "uint256", false, 0},
{"_belly", "bool", true, true}
]
}
"""
@spec find_and_decode([FunctionSelector.t()], topic, topic, topic, topic, binary) ::
{FunctionSelector.t(), [event_value]}
def find_and_decode(function_selectors, topic1, topic2, topic3, topic4, data) do
with {:ok, method_id, _rest} <- Util.split_method_id(topic1),
{:ok, selector} when not is_nil(selector) <-
Util.find_selector_by_method_id(function_selectors, method_id) do
input_topics = [topic2, topic3, topic4]

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_raw(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.is_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_raw([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
45 changes: 41 additions & 4 deletions lib/abi/function_selector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,30 @@ defmodule ABI.FunctionSelector do
* `:function` - Name of the function
* `:types` - Function's input types
* `:returns` - Function's return types
* `:method_id` - First four bits of the hashed function signature
* `:input_names` - Names of the input values (argument names)
* `:type` - The type of the selector. Events are part of the ABI, but are not considered functions
* `:inputs_index` - A list of true/false values denoting if each input is indexed. Only populated for events.
"""
@type t :: %__MODULE__{
function: String.t(),
method_id: String.t() | nil,
input_names: [String.t()],
types: [type],
returns: [type]
returns: [type],
type: :event | :function,
inputs_indexed: [boolean]
}

defstruct [:function, :method_id, input_names: [], types: [], returns: []]
defstruct [
:function,
:method_id,
:type,
:inputs_indexed,
input_names: [],
types: [],
returns: []
]

@doc """
Decodes a function selector to a struct.
Expand Down Expand Up @@ -145,7 +159,29 @@ defmodule ABI.FunctionSelector do
function: function_name,
types: input_types,
returns: output_types,
input_names: input_names
input_names: input_names,
type: :function
}

add_method_id(selector)
end

def parse_specification_item(%{"type" => "event"} = item) do
%{
"name" => event_name,
"inputs" => named_inputs
} = item

input_types = Enum.map(named_inputs, &parse_specification_type/1)
input_names = Enum.map(named_inputs, &Map.get(&1, "name"))
inputs_indexed = Enum.map(named_inputs, &Map.get(&1, "indexed"))

selector = %ABI.FunctionSelector{
function: event_name,
types: input_types,
input_names: input_names,
inputs_indexed: inputs_indexed,
type: :event
}

add_method_id(selector)
Expand All @@ -157,7 +193,8 @@ defmodule ABI.FunctionSelector do
method_id: nil,
input_names: [],
types: [],
returns: []
returns: [],
type: :function
}
end

Expand Down
22 changes: 22 additions & 0 deletions lib/abi/util.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ABI.Util do
def split_method_id(<<method_id::binary-size(4), rest::binary>>) do
{:ok, method_id, rest}
end

def split_method_id(_) do
{:error, :invalid_data}
end

def find_selector_by_method_id(function_selectors, method_id_target) do
function_selector =
Enum.find(function_selectors, fn %{method_id: method_id} ->
method_id == method_id_target
end)

if function_selector do
{:ok, function_selector}
else
{:error, :no_matching_function}
end
end
end
25 changes: 24 additions & 1 deletion priv/dog.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,28 @@
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
},

{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_from_human",
"type": "string"
},
{
"indexed": false,
"name": "_number",
"type": "uint256"
},
{
"indexed": true,
"name": "_belly",
"type": "bool"
}
],
"name": "WantsPets",
"type": "event"
}
]
4 changes: 4 additions & 0 deletions test/abi/event_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule ABI.EventTest do
use ExUnit.Case, async: true
doctest ABI.Event
end
Loading

0 comments on commit 054d433

Please sign in to comment.