defmodule Web3.ABI.Compiler do
@moduledoc """
Compile for ABI json file
"""
require Logger
alias Web3.Type.{Event, Function, Constructor}
defmacro __using__(opts) do
alias Web3.Type.{Event, Function}
abi_path = opts[:abi_path]
abi = parse_abi_file(abi_path)
event_defs =
for %Event{} = event_abi <- abi do
defevent(event_abi)
end
function_defs =
for %Function{} = function_abi <- abi do
deffunction(function_abi, opts)
end
quote do
@config unquote(opts)
@external_resource unquote(abi_path)
@abi unquote(Macro.escape(abi))
@contract_address Keyword.get(@config, :contract_address)
@priv_key Keyword.get(@config, :priv_key)
def abi(), do: @abi
def address(), do: @contract_address
Module.register_attribute(__MODULE__, :events, accumulate: true)
unquote(event_defs)
def events(), do: @events
def config(), do: @config
@events_lookup Map.new(@events)
def lookup(event_signature) do
@events_lookup[event_signature]
end
def decode_event(%{topics: [event_signature | _]} = log) do
case lookup(event_signature) do
nil ->
nil
event ->
Web3.Type.Event.decode_log(event, log)
end
end
unquote(function_defs)
end
end
def parse_abi_file(file_name) do
file_name
|> File.read!()
|> Jason.decode!(keys: :atoms)
|> Enum.map(&parse_abi/1)
|> Enum.reject(&is_nil/1)
end
def parse_abi(%{type: "constructor"} = abi) do
%Constructor{
inputs: parse_params(abi.inputs),
payable: abi[:payable],
state_mutability: parse_state_mutability(abi[:stateMutability])
}
end
def parse_abi(%{type: "event"} = abi) do
inputs = parse_event_params(abi.name, abi.inputs)
%Event{
name: String.to_atom(abi.name),
anonymous: abi.anonymous,
inputs: inputs,
signature: calc_signature(abi.name, inputs)
}
end
def parse_abi(%{type: "function"} = abi) do
%Function{
name: String.to_atom(abi.name),
inputs: parse_params(abi.inputs),
outputs: parse_params(abi.outputs),
constant: abi[:constant],
payable: abi[:payable],
state_mutability: parse_state_mutability(abi[:stateMutability])
}
end
# receive & fallback
def parse_abi(%{type: _}) do
nil
end
def calc_signature(name, inputs) do
[name, ?(, inputs |> Enum.map(&Web3.ABI.Types.name(elem(&1, 1))) |> Enum.join(","), ?)]
|> IO.iodata_to_binary()
|> ExKeccak.hash_256()
|> Web3.ABI.to_hex()
end
def parse_event_params(event_name, type_defs) do
type_defs
|> Enum.map(fn %{name: name, indexed: indexed} = type_def ->
if name == "" do
Logger.error("Event #{inspect(event_name)}: empty param name")
end
{String.to_atom(name), Web3.ABI.Types.parse(type_def), [indexed: indexed]}
end)
end
def parse_params(type_defs) do
type_defs
|> Enum.map(fn %{name: name} = type_def ->
param =
name
|> String.trim_leading("_")
|> String.to_atom()
{param, Web3.ABI.Types.parse(type_def)}
end)
end
def parse_state_mutability("view"), do: :view
def parse_state_mutability("pure"), do: :pure
def parse_state_mutability("payable"), do: :payable
def parse_state_mutability("nonpayable"), do: :nonpayable
def parse_state_mutability(_), do: :view
def defevent(%Web3.Type.Event{} = event) do
quote do
use Web3.Type.Event, event: unquote(event)
end
end
def deffunction(%Web3.Type.Function{} = function, opts \\ []) do
quote do
use Web3.Type.Function, function: unquote(function), opts: unquote(opts)
end
end
end