lib/web3.ex

defmodule Web3 do
  @moduledoc """
  TODO [ ] add web3 moduledoc
  """

  use Web3.Utils

  alias Web3.{HTTP, Dispatcher}

  defmacro __using__(opts) do
    quote do
      require Logger

      import unquote(__MODULE__)

      @before_compile unquote(__MODULE__)

      Module.register_attribute(__MODULE__, :registered_middleware, accumulate: true)
      Module.register_attribute(__MODULE__, :registered_methods, accumulate: true)
      Module.register_attribute(__MODULE__, :registered_contracts, accumulate: true)

      @default_middleware [
        Web3.Middleware.Parser,
        Web3.Middleware.RequestInspector,
        Web3.Middleware.ResponseFormatter
      ]

      @default_methods [
        {:eth_blockNumber, return_fn: :hex},
        {:eth_getBalance, args: 2, return_fn: :hex},
        {:eth_gasPrice, return_fn: :hex},
        {:eth_getTransactionReceipt, args: 1},
        {:eth_getBlockByHash, args: 2},
        {:eth_getBlockByNumber, args: 2},
        {:eth_getTransactionCount, args: 2, return_fn: :hex},
        {:eth_getLogs, args: 1},
        {:eth_sendRawTransaction, args: 1},
        {:eth_getCode, args: 2},
        {:net_version, return_fn: :integer},
        {:eth_getTransactionByHash, args: 1},
        {:eth_getUncleByBlockHashAndIndex, args: 2},
        {:eth_getTransactionByBlockHashAndIndex, args: 2},
        {:eth_getTransactionByBlockNumberAndIndex, args: 2},
        {:eth_getBlockTransactionCountByHash, args: 1},
        {:eth_getBlockTransactionCountByNumber, args: 1}
      ]

      @default_config [
        http: Web3.HTTP.HTTPoison,
        http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :web3]]
      ]

      @config compile_config(__MODULE__, @default_config, unquote(opts))
    end
  end

  defmacro __before_compile__(env) do
    # middleware
    default_middleware = env.module |> Module.get_attribute(:default_middleware, []) |> Enum.reverse()
    registered_middleware = env.module |> Module.get_attribute(:registered_middleware, []) |> Enum.reverse()
    middleware = Enum.reduce(default_middleware, registered_middleware, fn middleware, acc -> [middleware | acc] end)

    # methods
    default_methods = env.module |> Module.get_attribute(:default_methods, [])
    registered_methods = env.module |> Module.get_attribute(:registered_methods, [])
    methods = default_methods ++ registered_methods

    # contracts
    registered_contracts = env.module |> Module.get_attribute(:registered_contracts, [])

    global_config =
      env.module
      |> Module.get_attribute(:config)
      |> Keyword.put(:middleware, middleware)

    dispatch_defs =
      for {method, opts} <- methods do
        new_opts =
          global_config
          |> Keyword.merge(opts)

        defdispatch(method, new_opts)
      end

    contract_defs =
      for {contract_name, opts} <- registered_contracts do
        contract_name = Module.concat(__CALLER__.module, contract_name)

        new_opts =
          global_config
          |> Keyword.merge(opts)

        defcontract(contract_name, new_opts)
      end

    quote generated: true do
      # dispatch
      unquote(dispatch_defs)

      # contract
      unquote(contract_defs)

      def config(), do: unquote(global_config)

      @doc """
      Execute Contract

      TODO [ ] add contract doc

      """
      def execute_contract(requests, abi) do
        Web3.Contract.execute(requests, abi, unquote(global_config))
      end
    end
  end

  def json_rpc(payload, json_rpc_arguments), do: HTTP.json_rpc(payload, json_rpc_arguments)

  defp defdispatch(method, opts \\ []) do
    # validate method
    # :ok = parse_method(method)

    arg_number = Keyword.get(opts, :args, 0)
    method_name = Keyword.get(opts, :name, method)
    return_fn = Keyword.get(opts, :return_fn, :raw)
    middleware = Keyword.get(opts, :middleware, [])

    json_rpc_arguments = [
      http: Keyword.get(opts, :http),
      http_options: Keyword.get(opts, :http_options, []),
      rpc_endpoint: Keyword.get(opts, :rpc_endpoint)
    ]

    args = Macro.generate_arguments(arg_number, __MODULE__)

    quote do
      def unquote(method_name)(unquote_splicing(args)) do
        payload = %Dispatcher.Payload{
          json_rpc_arguments: unquote(json_rpc_arguments),
          args: unquote(args),
          method_name: unquote(method_name),
          method: unquote(method),
          return_fn: unquote(return_fn),
          middleware: unquote(middleware)
        }

        Dispatcher.dispatch(payload)
      end
    end
  end

  defp defcontract(contract_name, opts \\ []) do
    quote do
      defmodule unquote(contract_name) do
        use Web3.ABI.Compiler, unquote(opts)
      end
    end
  end

  defmacro middleware(middleware_module) do
    quote do
      @registered_middleware unquote(middleware_module)
    end
  end

  defmacro contract(contract_name, opts) do
    quote do
      @registered_contracts {unquote(contract_name), unquote(opts)}
    end
  end

  defmacro dispatch(method, opts) do
    # :ok = parse_method(method)

    opts = parse_opts(opts, [])

    quote do
      @registered_methods {unquote(method), unquote(opts)}
    end
  end

  def compile_config(module_name, default_config, opts) do
    config = Application.get_env(:web3, module_name, [])

    default_config
    |> Keyword.merge(config)
    |> Keyword.merge(opts)
  end

  @register_methods [
    :eth_blockNumber,
    :eth_getBalance,
    :eth_gasPrice,
    :eth_getTransactionReceipt
  ]

  def parse_method(method) do
    unless method in @register_methods do
      raise """
      unexpected dispatch parameter "#{method}"
      available params are: #{Enum.map_join(@register_methods, ", ", &to_string/1)}
      """
    else
      :ok
    end
  end

  @register_params [
    :name,
    :args,
    :return_fn
  ]

  defp parse_opts([{:name, alias_name} | opts], result) when is_binary(alias_name) do
    parse_opts(opts, [{:name, String.to_atom(alias_name)} | result])
  end

  defp parse_opts([{param, value} | opts], result) when param in @register_params do
    parse_opts(opts, [{param, value} | result])
  end

  defp parse_opts([{param, _value} | _opts], _result) do
    raise """
    unexpected dispatch parameter "#{param}"
    available params are: #{Enum.map_join(@register_params, ", ", &to_string/1)}
    """
  end

  defp parse_opts([], result), do: result
end