Skip to content

Commit 282e333

Browse files
authored
Merge pull request #4 from tsutsu/fixes-0.1.9
# 0.1.9 * Add support for parsing ABI specification documents (`.abi.json` files) * Reimplement function signature parsing using a BNF grammar * Fix potential stack overflow during encoding/decoding
2 parents 8ad184c + eed5645 commit 282e333

File tree

12 files changed

+304
-84
lines changed

12 files changed

+304
-84
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ erl_crash.dump
1818

1919
# Also ignore archive artifacts (built via "mix archive.build").
2020
*.ez
21+
22+
# Ignore generated code from yecc and leex
23+
src/*_lexer.erl
24+
src/*_parser.erl

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 0.1.9
2+
* Add support for parsing ABI specification documents (`.abi.json` files)
3+
* Reimplement function signature parsing using a BNF grammar
4+
* Fix potential stack overflow during encoding/decoding
15
# 0.1.8
26
* Fix ordering of elements in tuples
37
# 0.1.7
@@ -11,4 +15,4 @@
1115
# 0.1.3
1216
* Bugfix for tuples to properly handle head/tail encoding
1317
# 0.1.2
14-
* Add support for tuples, fixed-length and variable length arrays
18+
* Add support for tuples, fixed-length and variable length arrays

lib/abi.ex

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ defmodule ABI do
88
@doc """
99
Encodes the given data into the function signature or tuple signature.
1010
11+
In place of a signature, you can also pass one of the `ABI.FunctionSelector` structs returned from `parse_specification/1`.
12+
1113
## Examples
1214
1315
iex> ABI.encode("baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned])
@@ -24,18 +26,28 @@ defmodule ABI do
2426
iex> ABI.encode("(string)", [{"Ether Token"}])
2527
...> |> Base.encode16(case: :lower)
2628
"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b457468657220546f6b656e000000000000000000000000000000000000000000"
29+
30+
iex> File.read!("priv/dog.abi.json")
31+
...> |> Poison.decode!
32+
...> |> ABI.parse_specification
33+
...> |> Enum.find(&(&1.function == "bark")) # bark(address,bool)
34+
...> |> ABI.encode([<<1::160>> |> :binary.decode_unsigned, true])
35+
...> |> Base.encode16(case: :lower)
36+
"b85d0bd200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"
2737
"""
28-
def encode(function_signature, data) do
29-
ABI.TypeEncoder.encode(
30-
data,
31-
ABI.FunctionSelector.decode(function_signature)
32-
)
38+
def encode(function_signature, data) when is_binary(function_signature) do
39+
encode(ABI.Parser.parse!(function_signature), data)
40+
end
41+
def encode(%ABI.FunctionSelector{} = function_selector, data) do
42+
ABI.TypeEncoder.encode(data, function_selector)
3343
end
3444

3545
@doc """
3646
Decodes the given data based on the function or tuple
3747
signature.
3848
49+
In place of a signature, you can also pass one of the `ABI.FunctionSelector` structs returned from `parse_specification/1`.
50+
3951
## Examples
4052
4153
iex> ABI.decode("baz(uint,address)", "00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000001" |> Base.decode16!(case: :lower))
@@ -46,12 +58,73 @@ defmodule ABI do
4658
4759
iex> ABI.decode("(string)", "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b457468657220546f6b656e000000000000000000000000000000000000000000" |> Base.decode16!(case: :lower))
4860
[{"Ether Token"}]
61+
62+
iex> File.read!("priv/dog.abi.json")
63+
...> |> Poison.decode!
64+
...> |> ABI.parse_specification
65+
...> |> Enum.find(&(&1.function == "bark")) # bark(address,bool)
66+
...> |> ABI.decode("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" |> Base.decode16!(case: :lower))
67+
[<<1::160>>, true]
4968
"""
50-
def decode(function_signature, data) do
51-
ABI.TypeDecoder.decode(
52-
data,
53-
ABI.FunctionSelector.decode(function_signature)
54-
)
69+
def decode(function_signature, data) when is_binary(function_signature) do
70+
decode(ABI.Parser.parse!(function_signature), data)
71+
end
72+
def decode(%ABI.FunctionSelector{} = function_selector, data) do
73+
ABI.TypeDecoder.decode(data, function_selector)
5574
end
5675

76+
@doc """
77+
Parses the given ABI specification document into an array of `ABI.FunctionSelector`s.
78+
79+
Non-function entries (e.g. constructors) in the ABI specification are skipped. Fallback function entries are accepted.
80+
81+
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.
82+
83+
## Examples
84+
85+
iex> File.read!("priv/dog.abi.json")
86+
...> |> Poison.decode!
87+
...> |> ABI.parse_specification
88+
[%ABI.FunctionSelector{function: "bark", returns: nil, types: [:address, :bool]},
89+
%ABI.FunctionSelector{function: "rollover", returns: :bool, types: []}]
90+
91+
iex> [%{
92+
...> "constant" => true,
93+
...> "inputs" => [
94+
...> %{"name" => "at", "type" => "address"},
95+
...> %{"name" => "loudly", "type" => "bool"}
96+
...> ],
97+
...> "name" => "bark",
98+
...> "outputs" => [],
99+
...> "payable" => false,
100+
...> "stateMutability" => "nonpayable",
101+
...> "type" => "function"
102+
...> }]
103+
...> |> ABI.parse_specification
104+
[%ABI.FunctionSelector{function: "bark", returns: nil, types: [:address, :bool]}]
105+
106+
iex> [%{
107+
...> "inputs" => [
108+
...> %{"name" => "_numProposals", "type" => "uint8"}
109+
...> ],
110+
...> "payable" => false,
111+
...> "stateMutability" => "nonpayable",
112+
...> "type" => "constructor"
113+
...> }]
114+
...> |> ABI.parse_specification
115+
[]
116+
117+
iex> [%{
118+
...> "payable" => false,
119+
...> "stateMutability" => "nonpayable",
120+
...> "type" => "fallback"
121+
...> }]
122+
...> |> ABI.parse_specification
123+
[%ABI.FunctionSelector{function: nil, returns: nil, types: []}]
124+
"""
125+
def parse_specification(doc) do
126+
doc
127+
|> Enum.map(&ABI.FunctionSelector.parse_specification_item/1)
128+
|> Enum.filter(&(&1))
129+
end
57130
end

lib/abi/function_selector.ex

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ defmodule ABI.FunctionSelector do
1919
defstruct [:function, :types, :returns]
2020

2121
@doc """
22-
Decodes a function selector to struct. This is a simple version
23-
and we may opt to do format parsing later.
22+
Decodes a function selector to a struct.
2423
2524
## Examples
2625
@@ -82,23 +81,7 @@ defmodule ABI.FunctionSelector do
8281
}
8382
"""
8483
def decode(signature) do
85-
captures = Regex.named_captures(~r/(?<function>[a-zA-Z_$][a-zA-Z_$0-9]*)?\((?<types>(([^,]+),?)*)\)/, signature)
86-
87-
if captures["function"] != "" do
88-
# Encode as a function call
89-
%ABI.FunctionSelector{
90-
function: captures["function"],
91-
types: decode_raw(captures["types"]),
92-
returns: nil
93-
}
94-
else
95-
# Encode as a tuple
96-
%ABI.FunctionSelector{
97-
function: nil,
98-
types: [{:tuple, decode_raw(captures["types"])}],
99-
returns: nil
100-
}
101-
end
84+
ABI.Parser.parse!(signature, as: :selector)
10285
end
10386

10487
@doc """
@@ -108,55 +91,59 @@ defmodule ABI.FunctionSelector do
10891
10992
iex> ABI.FunctionSelector.decode_raw("string,uint256")
11093
[:string, {:uint, 256}]
94+
95+
iex> ABI.FunctionSelector.decode_raw("")
96+
[]
11197
"""
11298
def decode_raw(type_string) do
113-
type_string
114-
|> String.split(",", trim: true)
115-
|> Enum.map(&decode_type/1)
99+
{:tuple, types} = decode_type("(#{type_string})")
100+
types
116101
end
117102

118-
def decode_type(full_type) do
119-
cond do
120-
# Check for array type
121-
captures = Regex.named_captures(~r/(?<type>[a-z0-9]+)\[(?<element_count>\d*)\]/, full_type) ->
122-
type = decode_type(captures["type"])
123-
124-
if captures["element_count"] != "" do
125-
{element_count, ""} = Integer.parse(captures["element_count"])
126-
127-
{:array, type, element_count}
128-
else
129-
{:array, type}
130-
end
131-
# Check for tuples
132-
captures = Regex.named_captures(~r/\((?<types>[a-z0-9\[\]]+,?)+\)/, full_type) ->
133-
types =
134-
String.split(captures["types"], ",", trim: true)
135-
|> Enum.map(fn type -> decode_type(type) end)
136-
137-
{:tuple, types}
138-
true ->
139-
decode_single_type(full_type)
140-
end
103+
@doc false
104+
def parse_specification_item(%{"type" => "function"} = item) do
105+
%{
106+
"name" => function_name,
107+
"inputs" => named_inputs,
108+
"outputs" => named_outputs
109+
} = item
110+
111+
input_types = Enum.map(named_inputs, &parse_specification_type/1)
112+
output_types = Enum.map(named_outputs, &parse_specification_type/1)
113+
114+
%ABI.FunctionSelector{
115+
function: function_name,
116+
types: input_types,
117+
returns: List.first(output_types)
118+
}
141119
end
120+
def parse_specification_item(%{"type" => "fallback"}) do
121+
%ABI.FunctionSelector{
122+
function: nil,
123+
types: [],
124+
returns: nil
125+
}
126+
end
127+
def parse_specification_item(_), do: nil
142128

143-
def decode_single_type("uint" <> size_str) do
144-
size = case size_str do
145-
"" -> 256 # default
146-
_ ->
147-
{size, ""} = Integer.parse(size_str)
129+
defp parse_specification_type(%{"type" => type}), do: decode_type(type)
148130

149-
size
150-
end
131+
@doc """
132+
Decodes the given type-string as a single type.
151133
152-
{:uint, size}
153-
end
134+
## Examples
135+
136+
iex> ABI.FunctionSelector.decode_type("uint256")
137+
{:uint, 256}
154138
155-
def decode_single_type("bool"), do: :bool
156-
def decode_single_type("string"), do: :string
157-
def decode_single_type("address"), do: :address
158-
def decode_single_type(els) do
159-
raise "Unsupported type: #{els}"
139+
iex> ABI.FunctionSelector.decode_type("(bool,address)")
140+
{:tuple, [:bool, :address]}
141+
142+
iex> ABI.FunctionSelector.decode_type("address[][3]")
143+
{:array, {:array, :address}, 3}
144+
"""
145+
def decode_type(single_type) do
146+
ABI.Parser.parse!(single_type, as: :type)
160147
end
161148

162149
@doc """
@@ -203,6 +190,7 @@ defmodule ABI.FunctionSelector do
203190
end
204191
defp get_type(els), do: "Unsupported type: #{inspect els}"
205192

193+
@doc false
206194
@spec is_dynamic?(ABI.FunctionSelector.type) :: boolean
207195
def is_dynamic?(:bytes), do: true
208196
def is_dynamic?(:string), do: true

lib/abi/parser.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule ABI.Parser do
2+
@moduledoc false
3+
4+
@doc false
5+
def parse!(str, opts \\ []) do
6+
{:ok, tokens, _} = str |> String.to_charlist |> :ethereum_abi_lexer.string
7+
8+
tokens = case opts[:as] do
9+
nil -> tokens
10+
:type -> [{:"expecting type", 1} | tokens]
11+
:selector -> [{:"expecting selector", 1} | tokens]
12+
end
13+
14+
{:ok, ast} = :ethereum_abi_parser.parse(tokens)
15+
16+
case ast do
17+
{:type, type} -> type
18+
{:selector, selector_parts} -> struct!(ABI.FunctionSelector, selector_parts)
19+
end
20+
end
21+
end

lib/abi/type_decoder.ex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,16 @@ defmodule ABI.TypeDecoder do
147147
[{"awesome", true}]
148148
"""
149149
def decode_raw(encoded_data, types) do
150-
do_decode(types, encoded_data)
150+
do_decode(types, encoded_data, [])
151151
end
152152

153-
@spec do_decode([ABI.FunctionSelector.type], binary()) :: [any()]
154-
defp do_decode([], bin) when byte_size(bin) > 0, do: raise("Found extra binary data: #{inspect bin}")
155-
defp do_decode([], _), do: []
156-
defp do_decode([type|remaining_types], data) do
153+
@spec do_decode([ABI.FunctionSelector.type], binary(), [any()]) :: [any()]
154+
defp do_decode([], bin, _) when byte_size(bin) > 0, do: raise("Found extra binary data: #{inspect bin}")
155+
defp do_decode([], _, acc), do: Enum.reverse(acc)
156+
defp do_decode([type|remaining_types], data, acc) do
157157
{decoded, remaining_data} = decode_type(type, data)
158158

159-
[decoded | do_decode(remaining_types, remaining_data)]
159+
do_decode(remaining_types, remaining_data, [decoded | acc])
160160
end
161161

162162
@spec decode_type(ABI.FunctionSelector.type, binary()) :: {any(), binary()}
@@ -262,4 +262,4 @@ defmodule ABI.TypeDecoder do
262262
end
263263
end
264264

265-
end
265+
end

lib/abi/type_encoder.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ defmodule ABI.TypeEncoder do
115115
"000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000007617765736f6d6500000000000000000000000000000000000000000000000000"
116116
"""
117117
def encode_raw(data, types) do
118-
do_encode(types, data)
118+
do_encode(types, data, [])
119119
end
120120

121121
@spec encode_method_id(%ABI.FunctionSelector{}) :: binary()
@@ -133,12 +133,12 @@ defmodule ABI.TypeEncoder do
133133
init
134134
end
135135

136-
@spec do_encode([ABI.FunctionSelector.type], [any()]) :: binary()
137-
defp do_encode([], _), do: <<>>
138-
defp do_encode([type|remaining_types], data) do
136+
@spec do_encode([ABI.FunctionSelector.type], [any()], [binary()]) :: binary()
137+
defp do_encode([], _, acc), do: :erlang.iolist_to_binary(Enum.reverse(acc))
138+
defp do_encode([type|remaining_types], data, acc) do
139139
{encoded, remaining_data} = encode_type(type, data)
140140

141-
encoded <> do_encode(remaining_types, remaining_data)
141+
do_encode(remaining_types, remaining_data, [encoded | acc])
142142
end
143143

144144
@spec encode_type(ABI.FunctionSelector.type, [any()]) :: {binary(), [any()]}

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule ABI.Mixfile do
33

44
def project do
55
[app: :abi,
6-
version: "0.1.8",
6+
version: "0.1.9",
77
elixir: "~> 1.4",
88
description: "Ethereum's ABI Interface",
99
package: [
@@ -27,6 +27,7 @@ defmodule ABI.Mixfile do
2727
defp deps do
2828
[
2929
{:ex_doc, "~> 0.14", only: :dev, runtime: false},
30+
{:poison, "~> 3.1", only: [:dev, :test]},
3031
{:exth_crypto, "~> 0.1.4"}
3132
]
3233
end

mix.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
44
"exth_crypto": {:hex, :exth_crypto, "0.1.4", "11f9084dfd70d4f9e96f2710a472f4e6b23044b97530c719550c2b0450ffeb61", [], [{:binary, "~> 0.0.4", [hex: :binary, repo: "hexpm", optional: false]}, {:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}, {:libsecp256k1, "~> 0.1.3", [hex: :libsecp256k1, repo: "hexpm", optional: false]}], "hexpm"},
55
"keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [], [], "hexpm"},
6-
"libsecp256k1": {:hex, :libsecp256k1, "0.1.3", "57468b986af7c9633527875f71c7ca08bf4150b07b38a60d5bd48fba299ff6c1", [], [], "hexpm"}}
6+
"libsecp256k1": {:hex, :libsecp256k1, "0.1.3", "57468b986af7c9633527875f71c7ca08bf4150b07b38a60d5bd48fba299ff6c1", [], [], "hexpm"},
7+
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}}

0 commit comments

Comments
 (0)