lib/ethers/contract.ex

defmodule Ethers.Contract do
  @moduledoc """
  Dynamically creates modules for ABIs at compile time.

  ## How to use
  You can simply create a new module and call `use Ethers.Contract` in it with the desired parameters.

  ```elixir
  defmodule MyProject.Contract do
    use Ethers.Contract, abi_file: "path/to/abi.json"
  end
  ```

  After this, the functions in your contracts should be accessible just by calling
  ```elixir
  data = MyProject.Contract.example_function(...)

  # Use data to handle eth_call
  Ethers.Contract.call(data, to: "0xADDRESS", from: "0xADDRESS")
  {:ok, [...]}
  ```

  ## Valid `use` options
  - `abi`: Used to pass in the encoded/decoded json ABI of contract.
  - `abi_file`: Used to pass in the file path to the json ABI of contract.
  - `default_address`: Default contract deployed address. Can be overridden with `:to` option in every function.

  ## Execution Options
  These can be specified for all the actions by contracts.

  - `action`: Type of action for this function. Here are available values.
    - `:call` uses `eth_call` to call the function and get the result. Will not change blockchain state or cost gas.
    - `:send` uses `eth_sendTransaction` to call
    - `:estimate_gas` uses `eth_estimateGas` to estimate the gas usage of this transaction.
    - `:prepare` only prepares the `data` needed to make a transaction. Useful for Multicall.
  - `from`: The address of the wallet making this transaction. The private key should be loaded in the rpc server (For example: go-ethereum). Must be in `"0x..."` format.
  - `gas`: The gas limit for your transaction.
  - `rpc_client`: The RPC module implementing Ethereum JSON RPC functions. Defaults to `Ethereumex.HttpClient`
  - `rpc_opts`: Options to pass to the RCP client e.g. `:url`.
  - `to`: The address of the recipient contract. It will be defaulted to `default_address` if it was specified in Contract otherwise is required. Must be in `"0x..."` format.
  """

  require Ethers.ContractHelpers
  import Ethers.ContractHelpers

  @type action :: :call | :send | :prepare
  @type t_function_output :: %{
          data: binary,
          to: Ethers.Types.t_address(),
          selector: ABI.FunctionSelector.t()
        }
  @type t_event_output :: %{
          topics: [binary],
          address: Ethers.Types.t_address(),
          selector: ABI.FunctionSelector.t()
        }

  @default_constructor %{
    selector: %ABI.FunctionSelector{
      function: nil,
      method_id: nil,
      type: :constructor,
      inputs_indexed: nil,
      state_mutability: nil,
      input_names: [],
      types: [],
      returns: []
    }
  }

  defmacro __using__(opts) do
    compiler_module = __MODULE__

    quote do
      @before_compile unquote(compiler_module)
      Module.put_attribute(__MODULE__, :_ethers_using_opts, unquote(opts))
    end
  end

  defmacro __before_compile__(env) do
    module = env.module

    {opts, _} =
      module
      |> Module.get_attribute(:_ethers_using_opts)
      |> Code.eval_quoted([], env)

    {:ok, abi} = read_abi(opts)
    contract_binary = maybe_read_contract_binary(opts)
    default_address = Keyword.get(opts, :default_address)

    function_selectors = ABI.parse_specification(abi, include_events?: true)

    function_selectors_with_meta =
      Enum.map(function_selectors, fn %{function: function} = selector ->
        %{
          selector: selector,
          has_other_arities: Enum.count(function_selectors, &(&1.function == function)) > 1
        }
      end)

    constructor_ast =
      function_selectors_with_meta
      |> Enum.find(&(&1.selector.type == :constructor))
      |> then(&(&1 || @default_constructor))
      |> generate_method(module)

    functions_ast =
      function_selectors_with_meta
      |> Enum.filter(&(&1.selector.type == :function and not is_nil(&1.selector.function)))
      |> Enum.map(&generate_method(&1, module))

    events_mod_name = Module.concat(module, EventFilters)

    events =
      function_selectors_with_meta
      |> Enum.filter(&(&1.selector.type == :event))
      |> Enum.map(&generate_event_filter(&1, module))

    events_module_ast =
      quote context: module do
        defmodule unquote(events_mod_name) do
          @moduledoc "Events for `#{Macro.to_string(unquote(module))}`"

          defdelegate default_address, to: unquote(module)
          unquote(events)
        end
      end

    extra_ast =
      quote context: module do
        def __contract_binary__, do: unquote(contract_binary)

        @doc """
        Default address of the contract. Returns `nil` if not specified.

        To specify a default address see `Ethers.Contract`
        """
        @spec default_address() :: Ethers.Types.t_address() | nil
        def default_address, do: unquote(default_address)
      end

    [extra_ast, constructor_ast | functions_ast] ++ [events_module_ast]
  end

  @doc false
  @spec perform_action(action(), map, Keyword.t()) ::
          {:ok, [term]}
          | {:ok, Ethers.Types.t_hash()}
          | {:ok, Ethers.Contract.t_function_output()}
          | {:error, term()}
  def perform_action(action, params, overrides)

  def perform_action(:call, params, overrides),
    do: Ethers.RPC.call(params, overrides)

  def perform_action(:send, params, overrides),
    do: Ethers.RPC.send(params, overrides)

  def perform_action(:estimate_gas, params, overrides),
    do: Ethers.RPC.estimate_gas(params, overrides)

  def perform_action(:prepare, params, overrides),
    do: {:ok, Enum.into(overrides, params)}

  def perform_action(action, _params, _overrides),
    do: raise(ArgumentError, "Invalid action: #{inspect(action)}")

  ## Helpers

  @spec generate_method(map(), atom()) :: any()
  defp generate_method(%{selector: %ABI.FunctionSelector{type: :constructor} = selector}, mod) do
    func_args = generate_arguments(mod, selector.types, selector.input_names)

    func_input_types =
      selector.types
      |> Enum.map(&Ethers.Types.to_elixir_type/1)

    quote context: mod, location: :keep do
      @doc """
      Prepares contract constructor values.

      To deploy a contracts see `Ethers.deploy/3`.

      ## Parameters
      #{unquote(document_types(selector.types, selector.input_names))}
      """
      @spec constructor(unquote_splicing(func_input_types)) :: binary()
      def constructor(unquote_splicing(func_args)) do
        args =
          unquote(func_args)
          |> Enum.zip(unquote(Macro.escape(selector.types)))
          |> Enum.map(fn {arg, type} -> Ethers.Utils.prepare_arg(arg, type) end)

        unquote(Macro.escape(selector))
        |> ABI.encode(args)
        |> Ethers.Utils.hex_encode(false)
      end
    end
  end

  defp generate_method(
         %{
           selector: %ABI.FunctionSelector{type: :function} = selector,
           has_other_arities: has_other_arities
         } = _function_data,
         mod
       ) do
    name =
      selector.function
      |> Macro.underscore()
      |> String.to_atom()

    bang_fun_name = String.to_atom("#{name}!")

    func_args = generate_arguments(mod, selector.types, selector.input_names)

    func_input_types =
      selector.types
      |> Enum.map(&Ethers.Types.to_elixir_type/1)

    func_return_typespec =
      selector.returns
      |> Enum.map(&Ethers.Types.to_elixir_type/1)
      |> then(fn
        [] -> []
        list -> Enum.reduce(list, &{:|, [], [&1, &2]})
      end)

    default_action = get_default_action(selector)

    overrides = get_overrides(mod, has_other_arities)

    quote context: mod, location: :keep do
      @doc """
      Executes `#{unquote(human_signature(selector))}` on the contract.

      Default action for this function is `#{inspect(unquote(default_action))}`.
      To override default action see Execution Options in `Ethers.Contract`.

      ## Parameters
      #{unquote(document_types(selector.types, selector.input_names))}
      - overrides: Overrides and options for the call. See Execution Options in `Ethers.Contract`.

      ## Return Types
      #{unquote(document_types(selector.returns))}
      """
      @spec unquote(name)(unquote_splicing(func_input_types), Keyword.t()) ::
              {:ok, [unquote(func_return_typespec)]}
              | {:ok, Ethers.Types.t_hash()}
              | {:ok, Ethers.Contract.t_function_output()}
              | {:error, term()}
      def unquote(name)(unquote_splicing(func_args), unquote(overrides)) do
        args =
          unquote(func_args)
          |> Enum.zip(unquote(Macro.escape(selector.types)))
          |> Enum.map(fn {arg, type} -> Ethers.Utils.prepare_arg(arg, type) end)

        data =
          unquote(Macro.escape(selector))
          |> ABI.encode(args)
          |> Ethers.Utils.hex_encode()

        params =
          %{
            data: data,
            selector: unquote(Macro.escape(selector))
          }
          |> maybe_add_to_address(__MODULE__)

        {action, overrides} = Keyword.pop(overrides, :action, unquote(default_action))

        Ethers.Contract.perform_action(action, params, overrides)
      end

      @doc """
      Same as `#{unquote(name)}/#{unquote(Enum.count(func_args) + 1)}` but raises `Ethers.ExecutionError` on errors.
      """
      @spec unquote(bang_fun_name)(unquote_splicing(func_input_types), Keyword.t()) ::
              [unquote(func_return_typespec)]
              | Ethers.Types.t_hash()
              | Ethers.Contract.t_function_output()
              | no_return
      def unquote(bang_fun_name)(unquote_splicing(func_args), unquote(overrides)) do
        case unquote(name)(unquote_splicing(func_args), overrides) do
          {:ok, result} ->
            result

          {:error, reason} ->
            raise Ethers.ExecutionError,
              error: reason,
              function: unquote(name),
              args: unquote(func_args)
        end
      end
    end
  end

  defp generate_event_filter(
         %{
           selector: %ABI.FunctionSelector{type: :event} = selector,
           has_other_arities: has_other_arities
         } = _function_data,
         mod
       ) do
    name =
      selector.function
      |> Macro.underscore()
      |> String.to_atom()

    func_args = generate_arguments(mod, selector.inputs_indexed, selector.input_names)

    {indexed_types, non_indexed_types} =
      selector.types
      |> Enum.zip(selector.inputs_indexed)
      |> Enum.reduce({[], []}, fn
        {type, true}, {indexed, non_indexed} ->
          {indexed ++ [type], non_indexed}

        {type, false}, {indexed, non_indexed} ->
          {indexed, non_indexed ++ [type]}
      end)

    func_input_typespec =
      indexed_types
      |> Enum.map(&Ethers.Types.to_elixir_type/1)

    selector = %{selector | returns: non_indexed_types}

    topic_0 =
      selector
      |> ABI.FunctionSelector.encode()
      |> Ethers.keccak_module().hash_256()
      |> Ethers.Utils.hex_encode()

    overrides = get_overrides(mod, has_other_arities)

    quote context: mod, location: :keep do
      @doc """
      Create event filter for `#{unquote(human_signature(selector))}` 

      For each indexed parameter you can either pass in the value you want to 
      filter or `nil` if you don't want to filter.

      ## Parameters
      #{unquote(document_types(indexed_types, selector.input_names))}
      - overrides: Overrides and options for the call.
        - `address`: The address or list of addresses of the originating contract(s). (**Optional**)

      ## Event Data Types
      #{unquote(document_types(selector.types, selector.input_names))}
      """
      @spec unquote(name)(unquote_splicing(func_input_typespec), Keyword.t()) ::
              Ethers.Contract.t_event_output()
      def unquote(name)(unquote_splicing(func_args), unquote(overrides)) do
        address = Keyword.get(overrides, :address, __MODULE__.default_address())

        sub_topics =
          Enum.zip(unquote(Macro.escape(selector.types)), unquote(func_args))
          |> Enum.map(fn
            {_, nil} ->
              nil

            {type, value} ->
              value
              |> Ethers.Utils.prepare_arg(type)
              |> List.wrap()
              |> ABI.TypeEncoder.encode([type])
              |> Ethers.Utils.hex_encode()
          end)

        %{
          topics: [unquote(topic_0) | sub_topics],
          address: address,
          selector: unquote(Macro.escape(selector))
        }
      end
    end
  end
end