lib/web3/abi.ex

defmodule Web3.ABI do
  @moduledoc """
  ABI is a module for encoding and decoding Ethereum Contract.

  Web3.ABI inspired by

    - [ABX](https://github.com/Kabie/abx)
    - [ExABI](https://github.com/poanetwork/ex_abi)

  """

  require Logger

  @doc false
  def to_hex(bytes) when is_binary(bytes), do: "0x" <> Base.encode16(bytes, case: :lower)

  @doc false
  def unhex("0x" <> hex), do: Base.decode16!(hex, case: :mixed)

  basic_types =
    [:address, :bool, :string, :bytes]
    |> Enum.map(&{to_string(&1), &1})

  int_types =
    for i <- 1..32 do
      {"int#{i * 8}", {:int, i * 8}}
    end

  uint_types =
    for i <- 1..32 do
      {"uint#{i * 8}", {:uint, i * 8}}
    end

  bytes_types =
    for i <- 1..32 do
      {"bytes#{i}", {:bytes, i}}
    end

  all_types = basic_types ++ int_types ++ uint_types ++ bytes_types

  # all_type_definitions =
  #   all_types
  #   |> Keyword.values()
  #   |> Enum.reduce(fn type, acc ->
  #     quote do
  #       unquote(type) | unquote(acc)
  #     end
  #   end)

  # @type types() :: unquote(all_type_definitions)

  def parse_type(%{type: "tuple", components: inner_types}) do
    {:tuple, inner_types |> Enum.map(&parse_type/1)}
  end

  for {name, type} <- all_types do
    def parse_type(%{type: unquote(name)}) do
      unquote(type)
    end
  end

  def parse_type(%{type: type_name} = type_def) do
    case Regex.run(~r/(.*)\[(\d*)\]/, type_name) do
      [_, inner_type, ""] ->
        {:array, parse_type(%{type_def | type: inner_type})}

      [_, inner_type, n] ->
        {:array, parse_type(%{type_def | type: inner_type}), String.to_integer(n)}

      _ ->
        Logger.warn("Unsupported type name: #{type_name}")
        String.to_atom(type_name)
    end
  end

  for {name, type} <- all_types do
    def type_name(unquote(type)) do
      unquote(name)
    end
  end

  def type_name({:array, inner_type}) do
    type_name(inner_type) <> "[]"
  end

  def type_name({:array, inner_type, n}) do
    "#{type_name(inner_type)}[#{n}]"
  end

  def type_name({:tuple, inner_types}) do
    "(#{inner_types |> Enum.map(&type_name/1) |> Enum.join(",")})"
  end

  def type_name(type) do
    Logger.warn("Unsupported type: #{inspect(type)}")
    to_string(type)
  end
end