lib/w3ws/abi.ex

defmodule W3WS.ABI do
  @moduledoc """
  Ethereum ABI Functions
  """

  @doc """
  Loads ABIs from an `Enumerable` of abi file paths

  ## Examples

      iex> from_files(["./test/support/files/test_abi.json"])
      [
        %ABI.FunctionSelector{
          type: :event,
          function: "Transfer",
          method_id: <<221, 242, 82, 173>>,
          input_names: ["from", "to", "value"],
          inputs_indexed: [false, false, false],
          types: [:address, :address, {:uint, 256}]
        }
      ]
  """
  def from_files(paths) do
    Enum.flat_map(paths, fn path ->
      path
      |> File.read!()
      |> Jason.decode!()
      |> ABI.parse_specification(include_events?: true)
      |> filter_abi_events()
    end)
  end

  @doc """
  Loads an ABI from a json decoded ABI spec

  ## Examples

      iex> from_abi([
      ...>   %{
      ...>     "name" => "Transfer",
      ...>     "type" => "event",
      ...>     "inputs" => [
      ...>       %{
      ...>         "name" => "from",
      ...>         "type" => "address",
      ...>         "indexed" => false,
      ...>         "internalType" => "address"
      ...>       },
      ...>       %{
      ...>         "name" => "to",
      ...>         "type" => "address",
      ...>         "indexed" => false,
      ...>         "internalType" => "address"
      ...>       },
      ...>       %{
      ...>         "name" => "value",
      ...>         "type" => "uint256",
      ...>         "indexed" => false,
      ...>         "internalType" => "uint256"
      ...>       }
      ...>     ]
      ...>   }
      ...> ])
      [
        %ABI.FunctionSelector{
          type: :event,
          function: "Transfer",
          method_id: <<221, 242, 82, 173>>,
          input_names: ["from", "to", "value"],
          inputs_indexed: [false, false, false],
          types: [:address, :address, {:uint, 256}]
        }
      ]
  """
  def from_abi(abi) do
    abi
    |> ABI.parse_specification(include_events?: true)
    |> filter_abi_events()
  end

  defp filter_abi_events(abi) do
    Enum.filter(abi, fn %ABI.FunctionSelector{type: type} -> type == :event end)
  end

  @doc """
  Decodes an event from a raw event

  ## Examples

      iex> decode_event(
      ...>   "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000959922be3caee4b8cd9a407cc3ac1c251c2007b10000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000034152420000000000000000000000000000000000000000000000000000000000",
      ...>   [
      ...>      %ABI.FunctionSelector{
      ...>        function: "TokenSupportChange",
      ...>        method_id: <<1, 72, 203, 165>>,
      ...>        type: :event,
      ...>        inputs_indexed: [false, false, false, false],
      ...>        state_mutability: nil,
      ...>        input_names: ["supported", "token", "symbol", "decimals"],
      ...>        types: [:bool, :address, :string, {:uint, 8}],
      ...>        returns: [],
      ...>        return_names: []
      ...>      }
      ...>   ],
      ...>   ["0x0148cba56e5d3a8d32fbcea206eae9e449ec0f0def4f642994b3edcd38561deb"]
      ...> )
      {:ok, 
        %ABI.FunctionSelector{
          function: "TokenSupportChange",
          method_id: <<1, 72, 203, 165>>,
          type: :event,
          inputs_indexed: [false, false, false, false],
          state_mutability: nil,
          input_names: ["supported", "token", "symbol", "decimals"],
          types: [:bool, :address, :string, {:uint, 8}],
          returns: [],
          return_names: []
        }, 
        %{
          "decimals" => 18, 
          "supported" => true, 
          "symbol" => "ARB", 
          "token" => "0x959922be3caee4b8cd9a407cc3ac1c251c2007b1"
        }
      }
  """
  @spec decode_event(binary(), list(%ABI.FunctionSelector{}), [binary()]) ::
          {:ok, %ABI.FunctionSelector{}, map()} | {:error, any()}
  def decode_event(data, abi, topics) do
    topics =
      Enum.map(topics, fn
        nil -> nil
        topic -> W3WS.Util.from_hex(topic)
      end)

    case ABI.Event.find_and_decode(
           abi,
           Enum.at(topics, 0),
           Enum.at(topics, 1),
           Enum.at(topics, 2),
           Enum.at(topics, 3),
           W3WS.Util.from_hex(data)
         ) do
      {:error, _} = err -> err
      {selector, event_data} -> {:ok, selector, decode_data(event_data)}
    end
  end

  @doc """
  Encodes a list of topics into their keccak hex representation

  ## Examples

      iex> encode_topics([
      ...>   "0x0000000000000000000000000000000000000000000000000000000000000001", 
      ...>   "0x0000000000000000000000000000000000000000000000000000000000000002",
      ...>   "SomeEvent",
      ...>   ["SomeEvent(uint8,uint8)"],
      ...>   "MissingAbiEvent(uint8)",
      ...>  nil
      ...> ], [
      ...>   %ABI.FunctionSelector{
      ...>     function: "SomeEvent",
      ...>     method_id: <<1, 72, 203, 165>>,
      ...>     type: :event,
      ...>     inputs_indexed: [false, false],
      ...>     state_mutability: nil,
      ...>     input_names: ["a", "b"],
      ...>     types: [{:uint, 8}, {:uint, 8}],
      ...>     returns: [],
      ...>     return_names: []
      ...>   } 
      ...> ])
      [
        "0x0000000000000000000000000000000000000000000000000000000000000001", 
        "0x0000000000000000000000000000000000000000000000000000000000000002", 
        "0xf4907308003e0ac1411f27720554a08b629260c5bcd94e153d38a3ad5d4ce8ad", 
        ["0xf4907308003e0ac1411f27720554a08b629260c5bcd94e153d38a3ad5d4ce8ad"], 
        "0x961ac6e850917325cc201160e6c6a650f0be9ec0fcae82c74d760ab6a9c0e7b0", 
        nil
      ]
  """
  def encode_topics(topics, abi) do
    Enum.map(topics, &encode_topic(&1, abi))
  end

  defp encode_topic(nil = topic, _abi), do: topic

  defp encode_topic("0x" <> _rest = topic, _abi), do: topic

  defp encode_topic(topic, nil) when is_binary(topic) do
    raise "Unable to encode topic #{inspect(topic)} as no ABI was provided"
  end

  defp encode_topic(topic, abi) when is_binary(topic) do
    {selector_name, backup_selector} =
      if String.contains?(topic, "(") do
        # this is an event signature, so convert it to a FunctionSelector
        selector = %ABI.FunctionSelector{function: name} = ABI.FunctionSelector.decode(topic)
        {name, selector}
      else
        # must be an event name
        {topic, nil}
      end

    # find the authoritative selector in the ABI so find the selector
    selector =
      Enum.find(abi, fn
        %ABI.FunctionSelector{type: :event, function: name} -> selector_name == name
        _ -> false
      end)

    selector = if is_nil(selector), do: backup_selector, else: selector

    selector
    |> selector_signature()
    |> W3WS.Util.keccak(hex?: true)
  end

  defp encode_topic(sub_topics, abi) when is_list(sub_topics) do
    Enum.map(sub_topics, &encode_topic(&1, abi))
  end

  defp selector_signature(selector) do
    non_indexed_types =
      selector
      |> selector_fields()
      |> Enum.reject(& &1[:indexed])
      |> Enum.map(&Map.get(&1, :type))
      |> Enum.map(&ABI.FunctionSelector.encode_type/1)

    Enum.join([selector.function, "(", Enum.join(non_indexed_types, ","), ")"])
  end

  defp decode_data(event_data) do
    Enum.reduce(event_data, %{}, fn field = {name, _type, _indexed, _value}, acc ->
      Map.put(acc, name, decode_value(field))
    end)
  end

  defp decode_value({_name, "address", _indexed, value}), do: W3WS.Util.to_hex(value)
  defp decode_value({_name, _type, true, {:dynamic, value}}), do: W3WS.Util.to_hex(value)
  defp decode_value({_name, _type, _indexed, value}), do: value

  @doc """
  Returns a list of ABI fields for the given selector

  ## Examples

      iex> selector_fields(%ABI.FunctionSelector{
      ...>   function: "SomeEvent", 
      ...>   method_id: <<1, 72, 203, 165>>,
      ...>   type: :event,
      ...>   inputs_indexed: [false, false],
      ...>   input_names: ["a", "b"],
      ...>   types: [{:uint, 8}, {:uint, 8}],
      ...>   returns: [],
      ...>   return_names: []
      ...> })
      [
        %{name: "a", indexed: false, type: {:uint, 8}},
        %{name: "b", indexed: false, type: {:uint, 8}}
      ]
  """
  @spec selector_fields(selector :: %ABI.FunctionSelector{}) :: list(map())
  def selector_fields(selector = %ABI.FunctionSelector{inputs_indexed: nil, types: types}) do
    stream = Stream.repeatedly(fn -> false end)

    %{selector | inputs_indexed: Enum.take(stream, length(types))}
    |> selector_fields()
  end

  def selector_fields(selector = %ABI.FunctionSelector{input_names: [], types: types}) do
    stream = Stream.repeatedly(fn -> "" end)

    %{selector | input_names: Enum.take(stream, length(types))}
    |> selector_fields()
  end

  def selector_fields(%ABI.FunctionSelector{
        input_names: input_names,
        inputs_indexed: inputs_indexed,
        types: types
      }) do
    [input_names, inputs_indexed, types]
    |> Enum.zip()
    |> Enum.map(fn {name, indexed, type} -> %{name: name, indexed: indexed, type: type} end)
  end
end